اصول طراحی SOLID

دوشنبه 13 مرداد 1399

اصول طراحی SOLID سرنامی برای پنج اصل طراحی است: اصل تک وظیفه‌ای(SRP)، اصل تفکیک اینترفیس (ISP)، اصل بسته و باز (OCP)، اصل جایگزینی لیسکوف (LSP)، اصل وارونگی وابستگی (DIP).

اصول طراحی SOLID

اصل تک وظیفه‌ای (Single Responsibility Principle)

طبق SRP هر کلاس یا ماژول مسئولیت یک قسمت از عملکردهای ارائه شده توسط نرم‌افزار را بر عهده دارد. این عملکردها فقط باید یک دلیل برای تغییر داشته باشند و این در صورتی است که یک بخش از مسئولیت نیاز به تغییر داشته باشد.


    public interface IUser  
    {  
       void AddUser();  
       void RemoveUser();  
       void UpdateUser();  
       void Logger();  
       void Message();  
    }  

اگر نگاهی عمیق به متدهای فوق داشته باشیم، می‌توانیم به روشنی دریابیم که برای IUser متدهایی مثل Log() و Message() معنا ندارد تا بخشی از آن باشند. بنابراین ما آن را به اینترفیس‌های جداگانه می‌شکنیم.


    public interface IUser  
    {  
       void AddUser();  
       void RemoveUser();  
       void UpdateUser();  
    }  
    public interface ILog  
    {  
       void Logger();  
    }  
    public interface IMessage  
    {  
       void Message();  
    }  

از اینجا می‌توانیم بگوییم که هر سه اینترفیس مسئولیت‌های منحصربه‌فرد خود را انجام می‌دهند. اکنون ما از تزریق وابستگی (Dependency injection) برای پیاده‌سازی کدهای زیر استفاده می‌کنیم.

public class User : IUser  
{  
   public void AddUser()  
   {  
      Console.WriteLine("Added User");  
   }  
   public void RemoveUser()  
   {  
      Console.WriteLine("Removed User");  
   }  
   public void UpdateUser()  
   {  
      Console.WriteLine("User Updated");  
   }  
}  
   
public class Log : ILog  
{  
   public void Logger()  
   {  
      Console.WriteLine("Logged Error");  
   }  
}  
   
public class Msg : IMessage  
{  
   public void Message()  
   {  
      Console.WriteLine("Messaged Sent");  
   }  
}  
class Class_DI  
{  
   private readonly IUser _user;  
   private readonly ILog _log;  
   private readonly IMessage _msg;  
   public Class_DI(IUser user, ILog log, IMessage msg)  
   {  
      this._user = user;  
      this._log = log;  
      this._msg = msg;  
   }  
   public void User()  
   {  
      this._user.AddUser();  
      this._user.RemoveUser();  
      this._user.UpdateUser();  
   }  
   public void Log()  
   {  
      this._log.Logger();  
   }  
   public void Msg()  
   {  
      this._msg.Message();  
   }  
}  
   
class Example1  
{  
   public static void Main()  
   {  
      Class_DI di = new Class_DI(new User(), new Log(), new Msg());  
      di.User();  
      di.Log();  
      di.Msg();  
      Console.ReadLine();  
   }  
}  
   
************End of Single Responsibility Principle********* 

اصل تفکیک اینترفیس (Interface Segregation Principle)

ISP توضیح می‌دهد که هیچ کلاینتی نباید مجبور شود به متدهایی که از آن استفاده نمی‌کند وابسته شود. به جای داشتن یک اینترفیس کامل و چند منظوره باید آن را به اینترفیس‌های کوچک‌تر و مرتبط‌ تری تقسیم کنیم تا کلاینت‌ها فقط از متدهایی که مرتبط با آن‌ها است با خبر باشند.

نکته: ما قبلا ISP را با مثال قبلی از SRP پوشش داده‌ایم.


    public interface IUser {  
        void AddUser();  
        void RemoveUser();  
        void UpdateUser();  
        void Logger();  
        void Message();  
    }  

اینترفیس‌های بزرگ و چند منظوره به اینترفیس‌های مربوطه برای کلاینت تقسیم می‌شوند.

public interface IUser {  
    void AddUser();  
    void RemoveUser();  
    void UpdateUser();  
}  
public interface ILog {  
    void Logger();  
}  
public interface IMessage {  
    void Message();  
} ** ** ** ** ** ** End of Interface Segregation Principle ** ** ** ** * 

اصل باز و بسته (Open Closed Principle)

OCP بیان می‌کند که هر موجودیت نرم‌افزاری باید برای گسترش قابلیت‌هایش باز باشد اما برای تغییرات بسته باشد و اجازه این کار را ندهد.

در اینجا ما توسط یک برنامه کنسول ساده پاداش معلمان را محاسبه می‌کنیم.


    public class Teacher  
    {  
       public int EmpId;  
       public string Name;  
       public Teacher(int id, string name)  
       {  
          this.EmpId = id;  
          this.Name = name;  
       }  
       public decimal Bonus(decimal salary)  
       {  
          return salary * .2M;  
       }  
    }  
    class Example2  
    {  
       public static void Main()  
       {  
          Teacher teacher = new Teacher(101, "Zeko");  
          Console.WriteLine("Employee ID: {0} Name: {1} Bonus: {2}", teacher.EmpId,teacher.Name,teacher.Bonus(10000));  
          Console.ReadLine();  
       }  
    }   

Teacher Employee ID: 101, Name: Zeko, Bonus: 2000.0

حالا بیایید فرض کنیم افزایش محاسبه پاداش برای معلمان دائم و موقت داریم. برای پیاده‌سازی آن باید کلاس موجود و متد آن را تغییر داده و اصلاح کنیم. در اینجا به وضوح می‌توانیم بگوییم که این امر نقض‌کننده اصل Open Closed است.

نقض قانون


    public class Teacher  
    {  
       public int EmpId;  
       public string Name;  
       public string EmpType;  
       public Teacher(int id, string name, string emptype)  
       {  
          this.EmpId = id;  
          this.Name = name;  
          this.EmpType = emptype;  
       }  
       public decimal Bonus(decimal salary)  
       {  
          if(EmpType=="Permanent")  
             return salary * .2M;  
          else  
             return salary * .1M;  
       }  
    }  

Permanent Teacher Employee ID: 101, Name: Zeko, Bonus: 2000.0

Temporarty Teacher Employee ID: 102, Name: Priyanka, Bonus: 1000.0

پس ما باید چه کار کنیم؟

برای رعایت اصل OCP کلاس Teacher را به عنوان abstract class با داشتن Bonus() به عنوان abstract method می‌سازیم.


    public abstract class Teacher  
    {  
       public int EmpId;  
       public string Name;  
       public Teacher(int id, string name)  
       {  
          this.EmpId = id;  
          this.Name = name;  
       }  
       public abstract decimal Bonus(decimal salary);  
    }  

اکنون مدرسه می‌تواند به راحتی پاداش معلمان دائمی خود را با پیاده‌سازی کلاس زیر محاسبه کند.


    public class PermanentTeacher : Teacher  
    {  
       public PermanentTeacher(int id, string name):base(id,name)  
       {  
       }  
       public override decimal Bonus(decimal salary)  
       {  
          return salary * .2M;  
       }  
    }  
        
    class Example2  
    {  
    public static void Main()  
    {  
       Teacher teacher = new PermanentTeacher(101, "Zeko");  
       Console.WriteLine("Employee ID: {0} Name: {1} Bonus: {2}", teacher.EmpId,teacher.Name,teacher.Bonus(10000));  
       Console.ReadLine();  
    }  
    }  

مزیت استفاده از کلاس abstract در اینجا این است که اگر مدرسه در آینده تصمیم به دادن پاداش به معلمان موقت خود نیز کند دیگر لازم نیست کلاس Teacher را همانند قبل تغییر دهیم، زیرا اکنون برای گسترش باز است.

درست مثل Permanent Teacher یک کلاس دیگر TemporaryTeacher اضافه کنید.

پیاده‌سازی نهایی

public abstract class Teacher  
{  
   public int EmpId;  
   public string Name;  
   public Teacher(int id, string name)  
   {  
      this.EmpId = id;  
      this.Name = name;  
   }  
   public abstract decimal Bonus(decimal salary);  
}  
   
public class PermanentTeacher : Teacher  
{  
   public PermanentTeacher(int id, string name):base(id,name)  
   {  
   }  
   public override decimal Bonus(decimal salary)  
   {  
      return salary * .2M;  
   }  
}  
    
public class TemporaryTeacher : Teacher  
{  
   public TemporaryTeacher(int id, string name) : base(id, name)  
   {  
   }  
   public override decimal Bonus(decimal salary)  
   {  
      return salary * .1M;  
   }  
}  
    
class Example2  
{  
   public static void Main()  
   {  
      Teacher teacher = new PermanentTeacher(101, "Zeko");  
      Teacher teacher2 = new TemporaryTeacher(102, "Priyanka");  
      Console.WriteLine("Employee ID: {0} Name: {1} Bonus: {2}", teacher.EmpId,teacher.Name,teacher.Bonus(10000));  
      Console.WriteLine("Employee ID: {0} Name: {1} Bonus: {2}", teacher2.EmpId, teacher2.Name, teacher2.Bonus(10000));   
      Console.ReadLine();  
   }  
}  
   
Employee ID: 101 Name: Zeko       Bonus: 2000.0  
Employee ID: 102 Name: Priyanka Bonus: 1000.0  
   
Hence the Teacher  class is now open for extention but closed for modification which does not vioalte the OCP.  
   
   
************End of Open Closed Principle*********  

اصل جایگزینی لیسکوف (Liskov Substitution Principle)

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

در مثال قبلی از اصل باز و بسته، ما قانونی از LSP را دنبال کردیم که در واقع کلاس پایه Teacher را با کلاس ارث‌بری شده PermanentTeacher و TemporaryTeacher جایگزین کردیم.


    class Example2  
    {  
       public static void Main()  
       {  
          Teacher teacher = new PermanentTeacher(101, "Zeko");  
          Teacher teacher2 = new TemporaryTeacher(102, "Priyanka");  
          Console.WriteLine("Employee ID: {0} Name: {1} Bonus: {2}", teacher.EmpId,teacher.Name,teacher.Bonus(10000));  
          Console.WriteLine("Employee ID: {0} Name: {1} Bonus: {2}", teacher2.EmpId, teacher2.Name, teacher2.Bonus(10000));  
       Console.ReadLine();  
       }  
    }  

بنابراین، بیایید دوباره فرض کنیم که مدرسه خواستار استخدام معلم قراردادی است که از زمانی که این شخص قرارداد بسته و استخدام شده است هیچ پاداشی را دریافت نکرده است.

برای پیاده‌سازی این بیایید یک کلاس دیگری شبیه به کلاس Permanent و Temporary اضافه کنیم.


    public class ContractTeacher : Teacher  
    {  
       public ContractTeacher(int id, string name) : base(id, name)  
       {  
       }  
       public override decimal Bonus(decimal salary)  
       {  
          throw new NotImplementedException();  
       }  
    }  
       
       
    class Example2  
    {  
       public static void Main()  
       {  
          Teacher teacher = new PermanentTeacher(101, "Zeko");  
          Teacher teacher2 = new TemporaryTeacher(102, "Priyanka");  
          Teacher teacher3 = new ContractTeacher(103, "Partha");   
          Console.WriteLine("Employee ID: {0} Name: {1} Bonus: {2}", teacher.EmpId,teacher.Name,teacher.Bonus(10000));  
          Console.WriteLine("Employee ID: {0} Name: {1} Bonus: {2}", teacher2.EmpId, teacher2.Name, teacher2.Bonus(10000));  
          Console.WriteLine("Employee ID: {0} Name: {1} Bonus: {2}", teacher3.EmpId, teacher3.Name, teacher3.Bonus(50000));   
          Console.ReadLine();  
       }  
    }  

اگر سولوشن را اجرا کنید، اکنون می‌توانیم که LSP را نقض می‌کند زیرا نوع زیر گروه در اینجا یک اکسپشن پرتاب می‌کند.

راه‌حل

ادامه می‌دهیم و ساختار را اصلاح می‌کنیم و دو اینترفیس جداگانه می‌گذاریم. به کد زیر رجوع کنید.

پیاده‌سازی نهایی

interface IBonus  
{  
   decimal Bonus(decimal salary);  
}  
interface ITeacher  
{  
   int EmpId { get; set; }  
   string Name { get; set; }  
   decimal GetSalary();  
}  
public abstract class Teacher : ITeacher,IBonus  
{  
   public int EmpId { get ; set; }  
   public string Name { get ; set ; }  
   public Teacher(int id, string name)  
   {  
      this.EmpId = id;  
      this.Name = name;  
   }  
   public abstract decimal GetSalary();  
   public abstract decimal Bonus(decimal salary);  
}  
   
   
public class PermanentTeacher : Teacher  
{  
   public PermanentTeacher(int id, string name) : base(id, name)  
   {  
   }  
   public override decimal GetSalary()  
   {  
      return 10000;  
   }  
   public override decimal Bonus(decimal salary)  
   {  
      return salary * .2M;  
   }  
}  
public class TemporaryTeacher : Teacher  
{  
   public TemporaryTeacher(int id, string name) : base(id, name)  
   {  
   }  
   public override decimal GetSalary()  
   {  
      return 10000;  
   }  
   public override decimal Bonus(decimal salary)  
   {  
      return salary * .1M;  
   }  
}  
   
   
//This class will have no Bonus  
public class ContractTeacher : ITeacher  
{  
   public int EmpId { get ; set ; }  
   public string Name { get ; set; }  
   public ContractTeacher(int id, string name)  
   {  
      this.EmpId = id;  
      this.Name = name;  
   }  
   public decimal GetSalary()  
   {  
      return 5000;  
   }  
}  
   
   
class Example2  
{  
   public static void Main()  
   {  
      //Getting the Bonus detail with Salary  
      Teacher teacher = new PermanentTeacher(101, "Zeko");  
      Teacher teacher2 = new TemporaryTeacher(102, "Priyanka");  
      Console.WriteLine("Employee ID: {0} Name: {1} Salary: {2} Bonus:{3}",       teacher.EmpId,teacher.Name,teacher.GetSalary(),teacher.Bonus(teacher.GetSalary()));  
      Console.WriteLine("Employee ID: {0} Name: {1} Salary: {2} Bonus:{3}", teacher2.EmpId, teacher2.Name, teacher2.GetSalary(),       teacher2.Bonus(teacher2.GetSalary()));  
      //Getting details without Bonus  
      List<ITeacher> teachers = new List<ITeacher>();  
      teachers.Add(new PermanentTeacher(101, "Zeko"));  
      teachers.Add(new TemporaryTeacher(102, "Priyanka"));  
      teachers.Add(new ContractTeacher(103, "Partha"));  
      foreach (var obj in teachers)  
      {  
         Console.WriteLine("Employee ID: {0} Name: {1} Salary: {2} ", obj.EmpId, obj.Name, obj.GetSalary());  
      }  
      Console.ReadLine();  
   }  
}  
   
************End of Liskov Substitution Principle********* 

اصل وارونگی وابستگی (Dependency Inversion Principle)

طبق DIP، ماژول‌های سطح بالا نباید به ماژول‌های سطح پایین وابسته باشند، بلکه هر دو باید به abstracions وابسته باشند. علاوه بر این، abstraction نباید به جزئیات وابسته باشد. جزئیات باید به abstraction وابسته باشد.


    public class Message  
    {  
       public void SendMessage()  
       {  
          Console.WriteLine("Message Sent");  
       }  
    }  
    public class Notification  
    {  
       private Message _msg;  
       public Notification()  
       {  
       _msg = new Message();  
       }  
       public void PromotionalNotification()  
       {  
          _msg.SendMessage();  
       }  
    }  
    class Test_Notify  
    {  
       public static void Main()  
       {  
       Notification notify = new Notification();  
       notify.PromotionalNotification();  
       Console.WriteLine();  
       Console.ReadLine();  
       }  
    }  

کلاس Notification بالا کاملا به کلاس Message وابسته است، زیرا فقط یک نوتیفیکیشن را می‌تواند ارسال کند. اگر بخواهیم دو نوع پیام (Email,SMS) را معرفی کنیم، باید کلاس notification را نیز تغییر دهیم، از این رو، این Tighy Coupling (به ساختار یکدیگر وابسته می‌باشند) است.

در حال حاضر بهترین روش برای انجام ملزومات با پیاده‌سازی تزریق وابستگی است تا بتوانیم آن را Loose Coupling (به حداقل رساندن وابستگی‌ها) بسازیم.

پیاده‌سازی نهایی


    public interface IMessage  
    {  
       void SendMessage();  
    }  
    public class Email : IMessage  
    {  
       public void SendMessage()  
       {  
       Console.WriteLine("Send Email");  
       }  
    }  
    public class SMS : IMessage  
    {  
       public void SendMessage()  
       {  
       Console.WriteLine("Send Sms");  
       }  
    }  
    public class Notification  
    {  
       private IMessage _msg;  
       public Notification(IMessage msg)  
       {  
       this._msg = msg;  
       }  
       public void Notify()  
       {  
       _msg.SendMessage();  
       }  
    }  
    class Test_Notify  
    {  
       public static void Main()  
       {  
          Email email = new Email();  
          //passing dependency  
          Notification notify = new Notification(email);  
          notify.Notify();  
          SMS sms = new SMS();  
          //passing depeNotification(email)ndency  
          notify = new Notification(sms);  
          notify.Notify();  
          Console.WriteLine();  
          Console.ReadLine();  
       }  
    }  

ایمان مدائنی

نویسنده 1299 مقاله در برنامه نویسان
  • C#.net
  • 2k بازدید
  • 4 تشکر

کاربرانی که از نویسنده این مقاله تشکر کرده اند

در صورتی که در رابطه با این مقاله سوالی دارید، در تاپیک های انجمن مطرح کنید