قوانین SOLID

مفهوم S.O.L.I.D در برنامه نویسی موضوعیه که دونستنش برای هر برنامه نویسی خیلی مهمه جوری که شیوه نوشتن کدهاتون رو کاملاً تغییر میده. در جلوتر مثال‌های حل شده با زبان C# هست اما نگران نباشید تمامی این قواعد رو میتونید برای هر زبانی که مفاهیم اصلی شی گرایی رو دارد، استفاده نمایید.


چرا باید مفاهیم S.O.L.I.D. رو یاد بگیریم؟

چیزی که این وسط برای همه پیش میاد اینه که میگن من مفاهیم شی گرایی رو بلدم و دیگه نیازی به قواعد S.O.L.I.D. ندارم که یاد بگیرم. بذارید این دو مسئله رو از هم جدا کنیم. اگه بخوام یه مثال ساده بزنم، برای یادگیری و استفاده از هر دو مسئله اینه که شما تمام ابزارهای چوب بری رو در اختیار داشته باشی و بگی که خب نیازی نیست که علم چوب بری هم یاد بگیرم.
با یادگیری برنامه نویسی شئ گرا، در حقیقت یاد میگیرد که چطوری از کلاس ها استفاده کنید، چگونه برای یک کلاس خصوصیت تعریف کنیم و...، اما اینکه بتونیم از این قابلیت‌ها در مسیر درست استفاده کنیم داستان فرق میکنه.
S.O.L.I.D. در واقع یک چهارچوب منظم رو برای ما ایجاد میکنه که مکمل شی گرایی است و با حرکت در این مسیر میتونیم کدهای بهینه تری بنویسیم.

معمولا مفهوم الگوی طراحی با مفاهیم SOLID اشتباه میشود. من خودم هم اشتباه میگرفتم. تفاوت این دو مفهوم اینه که الگوهای طراحی، مجموعه ای از کدها و راه حل‌های از پیش نوشته شده هستند که هر کدام مشکلی رو حل می کنند، یعنی در شرایط خاص از الگوهای طراحی برای حل مشکل استفاده کرد. اما، SOLID قوانینی هستند که برای نوشتن کدها استفاده میشوند.

1) S: Single Responsibility Principle
2) O: Open/Closed Principle
3) L: Liskov Substitution Principle
4) I: Interface Segregation Principle
5) D: Dependency Inversion Principle

 


1) Single Responsibility
برنامه‌ای که قصد نوشتن اونو دارید باید مثل یک سازمان قانون مند عمل کنه! یک سازمان موفق جوریه که، همه وظایف درش مشخص هست، هر کسی وظایف خودش رو داره و هیچکس توی وظایف شخص دیگه دخالت یا ورود نمیکنه. اگر هم شخصی نیاز به انجام عملیات تو قسمت دیگه‌ای رو داشته باشه، این وظیفه به شخص مورد نظر در آن بخش محول میشه. برنامه ما هم باید به همین صورت عمل کنه. یعنی هر یک از بخش های برنامه ما دقیقاً و صرفاً یک وظیفه و یا یک عملیات خاص رو انجام بده. بعضی وقت ها که به گذشته بر میگردم و برنامه هایی که قبلاً نوشتن رو مرور میکنم، ناگهان یک Method و میبینم که حدود 400 خط کد براش نوشته شده! اما با تفکری که الان دارم، به نظرم متدها بیشتر از 10 الی 15 خط کد نیاز ندارن! به این خاطر که هر کلاس و هر Method از برنامه ای که قصد نوشتن اون رو دارم باید فقط یه وظیفه مشخص رو انجام بده و در صورت وابسته بودن به یک عملیات دیگه، انجام اون عملیات باید به بخش مربوطه تو نرم افزار محول بشه. با یک مثال این موضوع رو بیشتر بررسی میکنیم، کد زیر رو در نظر بگیرید:

 

public class CustomersRepository
{
    public void Add()
    {
        try
        {
            // add customer to database
        }
        catch (Exception ex)
        {
            System.IO.File.WriteAllText("d:\\errors.txt", ex.ToString());
        }
    }
}

 

در کد بالا، کلاسی با نام CustomersRepository داریم که عملیات اضافه کردن مشتری به Database در اون انجام میشه. اما در صورتی که خطایی در این روند رخ بده، خطای مربوطه در فایلی ثبت میشه. اما مشکل کجاست؟ متد Add در کلاس StudentRepository وظیفه ای غیر از وظیفه اصلی ای که برایش مشخص شده، یعنی اضافه کردن مشتری رو انجام می ده.

 

با توجه به قاعده Single Reposibility، کد بالا باید اصلاح و وظیفه ثبت خطا به قسمت دیگه‌ای از برنامه محول بشه. برای اینکار اول کلاس دیگه‌ای برای عملیات ثبت خطاها تعریف میکنیم:

 

public class FileLogger
{
    public void Log(string content)
    {
        System.IO.File.WriteAllText("d:\\errors.txt", content);
    }
}


در قدم بعدی کلاس CustomersRepository رو طوری تغییر میدیم که وظیفه ثبت خطا رو به کلاس FileLogger محول بشه:

 

public class CustomersRepository
{
    FileLogger logger = new FileLogger();

    public void Add()
    {
        try
        {
            // add customer to database
        }
        catch (Exception ex)
        {
            logger.Log(ex.ToString());
        }
    }
}

دقیقا به همین سادگی میتونیم قانون اول رو در کدهای خود پیاده کنیم.

 


2) Open Closed
این قانون در مورد یک موجودیت در برنامه میگه، یک موجودیت نرم افزاری (کلاس، متد و غیره) باید برای توسعه باز باشد ولی برای تغییر بسته. بذارید با پرسیدن سوال این قسمت رو شروع کنم، به نظرتون چطوری میتونیم رفتار یک برنامه رو تغییر بدیم، بدون اینکه کد رو دست بزنیم؟ یا بدون تغییر یک موجودیت کارکردشو عوض کنیم؟

 

فرض کنید که یک کلاس برای محصولات داریم به شکل زیر:

 

public class Product
{
    public string Name { get; set; }
    public string Price { get; set; }
}

 

حالا محصول رو به سه نوع مختلف تقسیم بندی میکنیم که بر اساس نوع، درصدی تخفیف برای محصول حساب می شود:

 

public class Product
{
    public int ProductType { get; set; }

    public string Name { get; set; }
    public int Price { get; set; }

    public double GetDiscount()
    {
        if (ProductType == 1)
        {
            return (Price/100)*5;
        }
        if (ProductType == 2)
        {
            return (Price/100)*10;
        }
        if (ProductType == 3)
        {
            return (Price/100)*15;
        }
        return 0;
    }
}

 

الان اگه بخواهیم نوع چهارمی از تخفیف رو برای کلاس Product داشته باشیم، باید یک دستور if دیگه به تابع GetDiscount اضافه کنیم، یعنی با هر بار تغییر، کلاس Product تغییر میکنه.

اینجا قانون Open Closed وارد میشه، چیزی که اول قانون مطرح شد این بود که یک کلاس برای تغییرات باید بسته باشد، یعنی اجازه تغییر کلاس برای افزودن امکانات جدید رو نداریم، اما راه واسه ایجاد Extension یا افزونه جدیدی برای کلاس باز است. در مثال بالا و بر اساس قانون Open Closed، یک کلاس پایه تعریف میکنیم و به ازای هر نوع محصول، یک کلاس فرزند براش ایجاد میکنیم:

 

public class Product
{
    public string Name { get; set; }
    public int Price { get; set; }

    public virtual double GetDiscount()
    {
        return 0;
    }
}

public class ProductType1 : Product
{
    public override double GetDiscount()
    {
        return (Price/100)*10;
    }
}

public class ProductType2 : Product
{
    public override double GetDiscount()
    {
        return (Price/100)*15;
    }
}

 

در صورت اضافه شدن نوع محصول یا نوع جدیدی از تخفیف، به جای تغییر کلاس Product، به راحتی یک کلاس فرزند ایجاد کرده و متد GetDiscount رو در آن Override می کنیم.

 


3) Liskov Substitution
همانطور که در قانون OpenClose گفته شد ما باید از وراثت استفاده کنیم و به کمک وراثت می‌تونیم کلاس‌های مشتق شده ایجاد کنیم تا متدهای کلاس پایه رو در خود جای دهند. سوالی که اینجا پیش میاد اینه که چطوری باید وراثت رو استفاده کنیم؟ منظورم اینه که قانونش چیه؟

پاسخ این سوالها در قانون Liskov هست. این قانون میگه که که اشیاء کلاس‌های مشتق شده (فرزندان) باید بتونند جایگزین اشیاء کلاس پایه خود (پدر) شوند. قبول دارم خیلی پیچیده است اما با مثال میریم جلو تا موضوع برامون جا بیوفته. کد زیر رو در نظر بگیرید:

 

public class CollectionBase
{
    public int Count { get; set; }
}

public class Array : CollectionBase
{
        
}

 

بر اساس قوانین شی گرایی، از کد بالا می تونید به صورت زیر استفاده کنید:

 

CollectionBase collection = new Array();
var items = collection.Count;

 

در حقیقت، شی Array داخل متغیری از نوع CollectionBase قرار داده شده و خوب تا اینجا مشکلی نیست، اما فرض کنید قرار است کلاس‌های دیگه‌ای از CollectionBase مشتق شوند که قابلیت اضافه کردن Item رو دارند، به طور مثال کلاس Array چون طول ثابتی داره دیگه نمیشه به آن Item جدیدی اضافه کرد، سپس کد بالا رو به صورت زیر تغییر می دهیم:

 

public class CollectionBase
{
    public int Count { get; set; }

    public virtual void Add(object item)
    {
            
    }
}

public class List : CollectionBase
{
    public override void Add(object item)
    {
        // add item to list
    }
}

public class Array : CollectionBase
{
    public override void Add(object item)
    {
        throw new InvalidOperationException();
    }
}

 

دقت کنید، متد Add رو داخل CollectionBase تعریف کردیم، کلاس List از متد Add پشتیبانی می کند اما کلاس آرایه به دلیلی که بالا گفتیم زمان فراخوانی متد Add، ایجاد خطا می کنید.

 

CollectionBase array = new Array();
CollectionBase list = new List();
list.Add(2); // works
array.Add(3); // throw exception

 

کد بالا بدون مشکل کامپایل می شود، اما زمانی که برنامه اجرا شود، زمان اضافه کردن آیتم به آرایه پیغام خطا دریافت میکنیم، با این اوصاف کد بالا قاعده LSP رو نقض کرد! یعنی ما متدی رو با ارث بری از کلاس پایه در فرزند ایجاد کردیم که نیازی نیست این متد در کلاس مشتق شده باشد.
همانطور که در بالا گفتیم در صورت استفاده از کلاس پایه به عنوان Data Type و قرار دادن شی‌ای از نوع فرزند در آن، برنامه بدون مشکل باید کار کند. اما راه حل این مشکل چیست؟ در اینجا روش استفاده از interface ها مطرح میشه، کد بالا رو به صورت زیر تغییر میدهیم:

 

public interface IList
{
    void Add(object item);
}

public class CollectionBase
{
    public int Count { get; set; }
}

public class List : CollectionBase, IList
{
    public void Add(object item)
    {
        // add item to list
    }
}

public class Array : CollectionBase
{
}

 

تغییرات کد رو دقت کنید، متد Add، به جای تعریف در کلاس CollectionBase، داخل یک interface به نام IList تعریف شده و کلاس List این interface رو پیاده سازی کرده است. با این کار، دیگه امکان فراخوانی متد Add برای کلاس Array وجود نداره. کد بالا مبتنی بر قانون Liskov هست. اینگونه میتوانیم متدهای لازمه برای هر کلاس مشتق شده رو دسته بندی کنیم و دیگه کلاس پدر رو مستقیم نسازیم:

 

Array array = new Array();
List list = new List();

array.Add(1); //عدم وجود
list.Add(1); //وجود 

 


4) Interface Segregation
مصرف کننده‌ها نباید وابسته به متدهایی باشند که آنها رو پیاده سازی نمیکنند. یعنی چی؟

زمانی که در حال نوشتن سرویسی هستید و در این کلاس سرویس دهنده از interface ها استفاده می کنید، دقت کنید که تنها باید اعضایی در دسترس بخش های برنامه باشند که به آن نیاز دارند! خب حالا چطوری اینکارو عملی کنیم؟ فرض کنید interface ای نوشتید که 10 تابع در آن تعریف شده، حالا شاید همه قسمت های برنامه به این 10 متد نیازی نداشته باشند (مثلا سرویسهای قسمت مدیریت سایت با قسمت نمایش عمومی سایت با هم تفاوت دارند).
در اینجا باید interface خودتون رو به interface های کوچکتر بشکنید تا فقط از interface هایی استفاده شود که به اونا واقعاً نیاز هست. برای مثال، کد زیر رو در نظر بگیرید

 

public interface IDatabaseManager
{
    void Add();
    void Remove(int id);
    void Persisit();
}

 

سه متد برای Interface بالا تعریف کردیم و در حال استفاده از این کد هستیم، حال برای قسمتی از برنامه نیاز به حذف گروهی موجودیت‌ها از database داریم، اولین کاری که می توانیم انجام دهیم اضافه کردن متدی با نام RemoveBatch به interface بالا است:

 

public interface IDatabaseManager
{
    void Add();
    void Remove(int id);
    void RemoveBatch(params int[] ids);
    void Persisit();
}

 

با این کار قانون Interface Segregation رو نقض کردیم، برای اصلاح کد بالا، می توانیم Interface جدیدی ایجاد کنیم که از interface قبلی مشتق شده است:

 

public interface IDatabaseManager
{
    void Add();
    void Remove(int id);
    void Persisit();
}

public interface IDbBatchOperations : IDatabaseManager
{
    void RemoveBatch(params int[] ids);
}

 

الان تفاوت IDatabaseManager با IDbBatchOperations اینه که IDbBatchOperations علاوه بر متدهای IDatabaseManager متد RemoveBatch رو هم پشتیبانی میکند. و به جای یه Interface چاق اون رو شکوندیم به قسمتهای کوچکتر.

 


5) Dependency Inversion
قاعده معکوس کردن وابستگی، که میگه کلاسهای سطح بالا نباید به کلاسهای سطح پایین وابسته باشند، هر دو باید به Interfaceها وابسته باشند. Interfaceها نباید وابسته به جزئیات باشند، بلکه جزئیات باید وابسته به Interfaceها باشند. به زبون ساده ترش میشه، وقتی ما با تکنیک های شی گرایی برنامه می نویسیم، حتما، کلاس‌هایی داریم که وابسته به کلاس‌های دیگر هستند. قاعده Single Responsiblity رو یادتونه؟ گفتیم هر کلاس باید فقط و فقط یک وظیفه خاص رو انجام بده و بقیه کارها رو به کلاس‌های مربوطه بسپره. دقت کنید که نباید ارتباط مستقیمی بین کلاس ها وجود داشته باشد!
مثال زیر رو ببنید:

 

public class EmailNotification
{
    public void Send(string message)
    {
        // send email
    }
}

public class DatabaseManager
{
    private EmailNotification notification;

    public DatabaseManager()
    {
        notification = new EmailNotification();
    }

    public void Add()
    {
        notification.Send("Record added to database!");
    }

    public void Remove()
    {
        notification.Send("Record removed to database!");
    }

    public void Persisit()
    {
        notification.Send("Changes submitted to database!");
    }
}

 

کلاسی داریم با نام DatabaseManager که با فراخوانی هر یک از متدهاش، یک ایمیل برای یک آدرس مشخص ارسال میشه. در کد بالا وظایف تقسیم بندی شده، یعنی قانون اول (SRP) رو در نظر گرفتیم، اما ارتباطی که میان کلاس DatabaseManager و کلاس EmailNotification وجود داره، مستقیمه. اگه بخوایم به جای ارسال رویداد بوسیله Email از پیامک استفاده کنیم، باید کلاس جدیدی تعریف بشه و کلاس DatabaseManager تغییر کنه تا رویدادها با پیامک ارسال بشه. اما با پیاده سازی مبتنی بر قاعده Dependency Inversion، این کار به راحتی امکان پذیره، برای این کار ابتدا یک interface با نام INotification تعریف میکنیم:

 

public interface INotification
{
    void Send(string message);
}

 

حالا، کلاس ما باید interface ای که در آن تعریف کردیم رو پیاده سازی کنه، در زیر دو کلاس EmailNotification و SMSNotification رو به شکل زیر تعریف میکنیم:

 

public class EmailNotification : INotification
{
    public void Send(string message)
    {
        // send email
    }
}

public class SMSNotification : INotification
{
    public void Send(string message)
    {
        // send sms
    }
}

 

حالا کلاس DatbaseManager رو جوری تغییر می دهیم تا وابستگی آن نسبت به یک کلاس از بین رفته و وابسته به interface تعریف شده باشد:

 

public class DatabaseManager
{
    private INotification notification;

    public DatabaseManager(INotification notification)
    {
        this.notification = notification;
    }

    public void Add()
    {
        notification.Send("Record added to database!");
    }

    public void Remove()
    {
        notification.Send("Record removed to database!");
    }

    public void Persisit()
    {
        notification.Send("Changes submitted to database!");
    }
}

 

با اعمال تغییرات بالا، کلاس DatabaseManager هیچ وابستگی به کلاس خاصی نداره و میشه زمان ساخت شی از روی اون، وابستگی مربوطه رو براش مشخص کرد:

 

DatabaseManager manager = new DatabaseManager(new SMSNotification());

 

و اگه خواستیم از سرویس ایمیل استفاده کنیم:

DatabaseManager manager = new DatabaseManager(new EmailNotification());

با تغییرات انجام شده، قانون Dependecy Inversion رو در کد خود اعمال کردیم.


نظرات (۲)

  • ناهید حسین زاده

    یکشنبه ۱۳۹۷/۳/۱۳

    خیلی عالی بود ممنووون. امیدوارم تو آموزش های بعدیتون سراغ دیزاین پترن ها برید.

  • امین محمدی

    یکشنبه ۱۳۹۷/۳/۱۳

    به زودی میذارم، ممنون از اینکه دنبال میکنی سایت رو!

برای نظر دادن کافیست وارد حساب کاربری خود شوید.