بررسی اصل Open Closed Principle در برنامه نویسی شی گرا

بررسی اصل Open Closed Principle

اصل دوم از سری اصول پنجگانه SOLID در طراحی شی‌گرا و کدنویسی شی‌گرا اصل Open Closed Principle می‌باشد، که به طور کوتاه به آن OCP نیز می‌گویند. براساس اصل OCP کلاس‌های یک برنامه شی‌گرا باید برای گسترش باز اما برای تغییر بسته باشد. اما این موضوع به چه معناست. دقت کنید که قسمت باز برای گسترش و یا Open to Extension به این معناست که شما باید کلاس‌هایتان را طوری طراحی کنید، که بتوانید Functionality های جدید را با ظهور نیازمندی‌های جدید به راحتی پیاده‌سازی کنید. و اما قسمت بسته برای تغییر یا Closed for Modification به این معناست که زمانی که شما یک کلاس را توسعه داده و کار آن را به اتمام رسانده‌اید، دیگر نباید نیاز به تغییر دادن آن داشته باشید، مگر به منظور رفع کردن باگ‌ها. ممکن است با خود فکر کنید، که این دو جمله از اصل OCP با یکدیگر در تناقض هستند. اما اگر کلاس‌های خود را و Dependency بین آن‌ها را به درستی سازماندهی کنید، برای پیاده‌سازی Functionality جدید می‌توانید بدون ویرایش کردن کدهای قبلی Functionality های جدید اضافه کنیم.

این موضوع اغلب با استفاده از Abstraction ها (استفاده کردن از Interface ها و Abstraction Class در سی شارپ) برای Dependency ها انجام می‌شود. دقت کنید که می‌توانیم Dependency های یک برنامه شی‌گرا را در قالب یک کلاس‌های Concrete (کلاس‌هایی که می‌توانیم از آن‌ها Object ایجاد کنیم) نیز انجام بدهیم. اما پیاده‌سازی Dependency ها در قالب Abstraction می‌تواند برنامه نهایی را Loosely Coupled کند. با استفاده از Abstraction ها یا Interface ها در سی شارپ می‌توانیم کلاس‌هایی را ایجاد کنیم که بدون نیاز به تغییر دادن Abstraction ها Functionality های جدید را به برنامه اضافه کند. اینگونه کلاس‌ها فقط نیاز دارند که Interface ها را پیاده‌سازی کنند. با استفاده کردن از اصل OCP در طراحی شی‌گرا نیاز به تغییر کردن Source کد در زمان پیاده‌سازی نیازمندی‌های جدید شدیداً کاهش پیدا می‌کند. این موضوع به نوبه خود خطر ایجاد شدن باگ‌ها در Source کد را کاهش می‌دهد. علاوه بر این استفاده از Interface ها برای پیاده‌سازی Dependency ها Coupling را کاهش داده و انعظاف‌پذیری را شدیداً کاهش می‌دهد.

مثال کاربردی

به منظور بررسی اصل OCP اقدام به ایجاد یک کلاس در سی شارپ خواهیم کرد که این اصل را نقض می‌کند و سپس با انجام ریفکتورینگ در این مثال، اصل OCP را برقرار می‌کنیم.


public class Logger
{
    public void Log(string message, LogType logType)
    {
        switch (logType)
        {
            case LogType.Console:
                Console.WriteLine(message);
                break;
 
            case LogType.File:
                // Code to send message to printer
                break;
        }
    }
}
 
 
public enum LogType
{
    Console,
    File
}

همانطور که در کد بالا می‌بینید یک کلاس برای Log کردن تعدادی پیام تعریف شده است. نام این کلاس Logger می‌باشد. کلاس Logger دارای یک متد است که به عنوان پارامتر ورودی، پیامی که باید Log بشود و همچنین نوع مکانیزم Log شدن را دریافت می‌کند. در درون این متد یک دستور Switch قرار داده‌ایم که براساس نوع Log که باید انجام بشود، نوع مکانیزم Log رفتار متفاوتی را انجام داده و خروجی را یا در یک پرینتر و یا در کنسول چاپ می‌کند. حال فرض کنید که پس از مدتی نیاز به اضافه کردن یک مکانیزم جدید برای Log کردن پیام‌ها دارید. برای مثال قصد داریم، علاوه بر پرینتر و کنسول بتوانیم آن‌ها را در یک دیتابیس نیز Log کنیم.

مشخص است که برای پیاده‌سازی این نیازمندی باید کدهایی که از قبل نوشته‌ایم را تغییر دهیم. در ابتدا باید یک LogType جدید در enum تعریف شده، اضافه کنیم و سپس باید دستور Switch در درون متد Log وجود دارد را تغییر داده تا بتوانیم مکانیزم Log کردن در دیتابیس را پیاده‌سازی کنیم. واضح است که این موضوع اصل OCP را نقض می‌کند چرا که براساس اصل OCP نباید برای پیاده‌سازی نیازمندی‌های جدید مجبور به تغییر دادن کدهایی باشیم که از قبل ایجاد کرده‌ایم. در ادامه به ریفکتور کردن این کد و پیاده‌سازی اصل OCP خواهیم پرداخت.

ریفکتور کردن کد

به منظور حل مشکل کد و برقراری اصل OCP می‌بایست در ابتدا enum ای که LogType نام دارد را حذف کنیم. چرا که این enum یک محدودیت برای روش Log کردن داده‌ها به حساب می‌آید. در ادامه برای هر مکانیزم Log کردن پیام‌ها یک کلاس جداگانه تعریف کنیم. در حال حاضر این به این معناست که در پروژه ما دو کلاس به نام ConsoleLogger و PrinterLogger که هر دو به نوبه خود وظیفه Log کردن پیام‌ ها در پرینتر و کنسول را دارند، تعریف خواهند شد.

علاوه بر این با ایجاد نیازمندی برای پیاده‌سازی مکانیزم‌های Log کردن جدید می‌توانیم کدهای جدید را اضافه کنیم. این موضوع بدون نیاز به تغییر کردن کدهایی که از قبل نوشته‌ایم می‌باشد. کد زیر مربوط به کلاس ConsoleLogger می باشد.


public class ConsoleLogger : IMessageLogger
{
    public void Log(string message)
    {
        Console.WriteLine(message);
    }
}

کد زیر کلاس PrinterLogger را نشان می دهد.


public class PrinterLogger : IMessageLogger
{
    public void Log(string message)
    {
        // Code to send message to printer
    }
}

در ادامه کلاس Logger تمامی عملیات Log کردن را انجام می‌دهد. این موضوع را با استفاده از یکی از کلاس‌های Logger مثل ConsoleLogger و PrinterLogger که در قسمت قبل تعریف کردیم، انجام خواهیم داد. علاوه بر این به منظور Tightly Couple نشدن کلاس Logger و انواع کلاس‌های Logger مثل ConsoleLogger و PrinterLogger از یک اینترفیس به نام IMessageLogger استفاده می‌کنیم. کد زیر این اینترفیس را نشان می دهد.


public interface IMessageLogger
{
    void Log(string message);
}

هر کلاس Logger نیازمند پیاده‌سازی این اینترفیس است و این اینترفیس به عنوان یک Dependency به درون تابع سازنده یا Constructor کلاس Logger تزریق می‌شود. کد زیر کلاس Logger را نشان می دهد.


public class Logger
{
    IMessageLogger _messageLogger;
 
    public Logger(IMessageLogger messageLogger)
    {
        _messageLogger = messageLogger;
    }
 
    public void Log(string message)
    {
        _messageLogger.Log(message);
    }
}

دقت کنید که Dependency های مربوط به کلاس Logger که می‌توانند ConsoleLogger و PrinterLogger باشند، به عنوان Concrete Class به درون کلاس Logger تزریق نشده است. بلکه یک Abstraction یا Interface که همان IMessageLogger می‌باشد، به درون تابع سازنده کلاس Logger تزریق شده است.

این موضوع باعث می‌شود که کلاس Logger بدون نیاز به دانستن اینکه چه مکانیزم Log کردن در حال حاضر انتخاب شده است، عملیات Log کردن را در درون متد Log انجام بدهد. حال در زمان نیاز به پیاده‌سازی یک مکانیزم Log جدید به راحتی یک کلاس دیگر برای مثال DataBaseLogger را تعریف میکنیم که از اینترفیس IMessageLogger استفاده کرده و می‌تواند در درون متد Log که از این اینترفیس به آن رسیده است، عملیات Log کردن در دیتابیس را انجام بدهد. در درون متد Log از کلاس Logger نیز متد Log مربوط به Dependency که به درون تابع سازنده تزریق شده است را صدا زده و پارامتر ورودی Message را به آن تحویل می‌دهیم. کد زیر نسخه ی ریفکتور شده ی کد اصلی را نشان می دهد.


public class Logger
{
    IMessageLogger _messageLogger;
 
    public Logger(IMessageLogger messageLogger)
    {
        _messageLogger = messageLogger;
    }
 
    public void Log(string message)
    {
        _messageLogger.Log(message);
    }
}
 
 
public interface IMessageLogger
{
    void Log(string message);
}
 
 
public class ConsoleLogger : IMessageLogger
{
    public void Log(string message)
    {
        Console.WriteLine(message);
    }
}
 
 
public class PrinterLogger : IMessageLogger
{
    public void Log(string message)
    {
        // Code to send message to printer
    }
}

منبع: وبسایت پرووید