دیزاین پترن سالید یک مجموعه از پنج اصل طراحی شی گرا است که به منظور افزایش تمیزی، انعطافپذیری و قابل توسعه بودن کد نوشته شده توسط برنامه نویسان ارائه شده است. این پنج اصل عبارتند از:
- Single responsibility principle: هر کلاس یا ماژول باید فقط یک مسئولیت داشته باشد و تغییرات در آن فقط باید به دلیل تغییر در آن مسئولیت رخ دهد.
- Open/closed principle: هر کلاس یا ماژول باید برای توسعه باز و برای تغییر بسته باشد. به این معنی که بتوان رفتار آن را با ارثبری یا تزریق وابستگی گسترش داد ولی نباید کد آن را تغییر داد.
- Liskov substitution principle: هر زیرکلاس باید قابل جایگزینی با کلاس پدرش باشد. به این معنی که هر جایی که یک شئ از کلاس پدر استفاده میشود، بتوان یک شئ از زیرکلاس آن را به جای آن قرار داد و رفتار سیستم تغییر نکند.
- Interface segregation principle: هر رابط باید خالص و کوچک باشد. به این معنی که هر رابط فقط شامل عملکردهای مربوط به یک مفهوم خاص باشد و نباید عملکردهای اضافی را به کلاسهای پیاده ساز آن تحمیل کند.
- Dependency inversion principle: هر کلاس یا ماژول باید به رابطی متصل باشد و نباید به جزئيات پياده سازي وابسته باشد.
Single responsibility principle:
فرض کنید که یک کلاس به نام Employee داریم که شامل اطلاعات و عملکردهای مربوط به یک کارمند است. اگر این کلاس علاوه بر مسئولیت مدیریت اطلاعات کارمند، مسئولیتهای دیگری مانند ذخیرهسازی در پایگاه داده، چاپ گزارش و ... را هم داشته باشد، این کلاس با اصل single responsibility principle نقض میشود. بهتر است که این کلاس فقط شامل اطلاعات و عملکردهای مربوط به یک کارمند باشد و مسئولیتهای دیگر را به کلاسهای دیگر واگذار کند. برای مثال:
// A class that violates the single responsibility principle public class Employee { public int Id { get; set; } public string Name { get; set; } public double Salary { get; set; } // This method is not related to the responsibility of managing employee information public void SaveToDatabase() { // Code to save the employee to the database } // This method is not related to the responsibility of managing employee information public void PrintReport() { // Code to print a report about the employee } } // A class that follows the single responsibility principle public class Employee { public int Id { get; set; } public string Name { get; set; } public double Salary { get; set; } } // A separate class for saving employees to the database public class EmployeeRepository { public void Save(Employee employee) { // Code to save the employee to the database } } // A separate class for printing reports about employees public class EmployeeReportPrinter { public void Print(Employee employee) { // Code to print a report about the employee } }
Open/closed principle:
فرض کنید که یک کلاس به نام Shape داریم که شامل یک متد به نام Area است که مساحت یک شکل را برمیگرداند. همچنین، دو زیرکلاس به نام Circle و Rectangle داریم که از کلاس Shape ارثبری میکنند و متد Area را پیادهسازی میکنند. اگر بخواهیم یک شکل جدید به نام Triangle را به سیستم اضافه کنیم، بهتر است که یک زیرکلاس جدید به نام Triangle بسازیم و از کلاس Shape ارثبری کنیم و متد Area را پیادهسازی کنیم. در این صورت، کلاس Shape برای توسعه باز و برای تغییر بسته است. اگر به جای این روش، بخواهیم در کلاس Shape چک کنیم که شکل ورودی چه نوعی است و براساس آن مساحت را محاسبه کنیم، در این صورت، هر بار که یک شکل جدید را اضافه کنیم، باید کد کلاس Shape را تغییر دهیم و این کلاس برای توسعه بسته و برای تغییر باز است. برای مثال:
// A class that follows the open/closed principle public abstract class Shape { public abstract double Area(); } public class Circle : Shape { public double Radius { get; set; } public override double Area() { return Math.PI * Radius * Radius; } } public class Rectangle : Shape { public double Length { get; set; } public double Width { get; set; } public override double Area() { return Length * Width; } } // A new class for a new shape that inherits from Shape and implements the Area method public class Triangle : Shape { public double Base { get; set; } public double Height { get; set; } public override double Area() { return 0.5 * Base * Height; } } // A class that violates the open/closed principle public class Shape { public enum ShapeType { Circle, Rectangle, Triangle } public ShapeType Type { get; set; } public double Radius { get; set; } // For Circle public double Length { get; set; } // For Rectangle public double Width { get; set; } // For Rectangle public double Base { get; set; } // For Triangle public double Height { get; set; } // For Triangle public double Area() { switch (Type) { case ShapeType.Circle: return Math.PI * Radius * Radius; case ShapeType.Rectangle: return Length * Width; case ShapeType.Triangle: return 0.5 * Base * Height; default: throw new Exception("Invalid shape type"); } } }
Liskov substitution principle:
فرض کنید که یک کلاس به نام Animal داریم که شامل یک متد به نام MakeSound است که صدای یک حیوان را تولید میکند. همچنین، دو زیرکلاس به نام Dog و Cat داریم که از کلاس Animal ارثبری میکنند و متد MakeSound را پیادهسازی میکنند. اگر بخواهیم یک شئ از کلاس Dog را با یک شئ از کلاس Animal جایگزین کنیم، هیچ مشکلی پیش نمیآید چون هر دو صدای واقعی خود را تولید میکنند. ولی اگر بخواهیم یک شئ از کلاس Cat را با یک شئ از کلاس Animal جایگزین کنیم، مشکل پیش میآید چون کلاس Cat صدای Dog را تولید میکند. در این صورت، کلاس Cat با اصل جانشینی لیسکف نقض میشود. بهتر است که کلاس Cat را به گونهای تغییر دهیم که صدای Cat را تولید کند. برای مثال:
// A class that follows the Liskov substitution principle public abstract class Animal { public abstract void MakeSound(); } public class Dog : Animal { public override void MakeSound() { Console.WriteLine("Woof"); } } public class Cat : Animal { public override void MakeSound() { Console.WriteLine("Meow"); } } // A class that violates the Liskov substitution principle public abstract class Animal { public abstract void MakeSound(); } public class Dog : Animal { public override void MakeSound() { Console.WriteLine("Woof"); } } public class Cat : Animal { public override void MakeSound() { Console.WriteLine("Woof"); // This is wrong, cats do not make woof sound } }
Interface segregation principle:
فرض کنید که یک رابط به نام IWorker داریم که شامل دو متد به نام Work و Eat است که قابلیت کار کردن و خوردن یک کارگر را نشان میدهد. همچنین، دو زیرکلاس به نام HumanWorker و RobotWorker داریم که از رابط IWorker پیادهسازی میکنند. اگر بخواهیم یک شئ از کلاس HumanWorker را با یک شئ از رابط IWorker جایگزین کنیم، هیچ مشکلی پیش نمیآید چون هر دو قابلیت کار کردن و خوردن دارند. ولی اگر بخواهیم یک شئ از کلاس RobotWorker را با یک شئ از رابط IWorker جایگزین کنیم، مشکل پیش میآید چون رباتها نمیتوانند بخورند. در این صورت، رابط IWorker با اصل تفکیک رابط نقض میشود. بهتر است که رابط IWorker را به دو رابط جداگانه به نام IWorkable و IEatable تقسیم کنیم و هر کلاس فقط از رابطهای مربوط به خود پیادهسازی کند. برای مثال:
// A interface that violates the interface segregation principle public interface IWorker { void Work(); void Eat(); } public class HumanWorker : IWorker { public void Work() { // Code to work } public void Eat() { // Code to eat } } public class RobotWorker : IWorker { public void Work() { // Code to work } public void Eat() { // This is wrong, robots do not eat throw new NotImplementedException(); } } // A interface that follows the interface segregation principle public interface IWorkable { void Work(); } public interface IEatable { void Eat(); } public class HumanWorker : IWorkable, IEatable { public void Work() { // Code to work } public void Eat() { // Code to eat } } public class RobotWorker : IWorkable { public void Work() { // Code to work } }
Dependency inversion principle:
به عنوان مثال، فرض کنید یک برنامه برای دفتر رسمی واردات و صادرات محصولات تولید شده در یک شرکت در نظر گرفته شده است.
برای پیادهسازی این برنامه، معمولاً از یک لایه دسترسی به داده (Data Access Layer) استفاده میشود که به اطلاعات واحدهای مختلف شرکت دسترسی دارد. این لایه ممکن است از خدماتی مانند پایگاه داده، فایلها و سرویسهای دیگر برای برقراری ارتباط با دادهها استفاده کند.
افزایش یکی از واحدهای شرکت به نام "افزار" باعث افزایش شایستگیهای لایه دسترسی به داده میشود. در حال حاضر برنامه برای اضافه کردن یک عضویت جدید به افزار از دیتابیس استفاده میکند. لایه دسترسی به داده مستقیماً به دیتابیس متصل میشود و اطلاعات عضویت را در آن ثبت میکند.
برای رعایت Dependency Inversion Principle، باید به جای اتصال مستقیم به دیتابیس، از رابطی مانند یک مخزن (Repository) استفاده کنید. این مخزن رابطی تعریف میکند که توابعی برای اضافه، حذف، بهروزرسانی و دریافت اطلاعات از دیتابیس را فراهم میکند. سپس یک کلاس محدود کننده (Concrete Class) برای این رابط پیادهسازی میشود که در آن مستقیماً به دیتابیس اتصال برقرار میکند.
مثال زیر نشان میدهد گونه Dependency Inversion Principle در برنامهریز سی شارپ اعمال میشود:
public interface IMembershipRepository { void AddMembership(Membership membership); } public class DatabaseMembershipRepository : IMembershipRepository { public void AddMembership(Membership membership) { // کد برای اتصال به دیتابیس و ثبت عضویت در آن } } public class Membership { // خصوصیتهای کلاس عضویت } public class MembershipService { private readonly IMembershipRepository _membershipRepository; public MembershipService(IMembershipRepository membershipRepository) { _membershipRepository = membershipRepository; } public void AddMembership(Membership membership) { _membershipRepository.AddMembership(membership); } } public class Program { public static void Main() { IMembershipRepository membershipRepository = new DatabaseMembershipRepository(); MembershipService membershipService = new MembershipService(membershipRepository); Membership newMembership = new Membership(); membershipService.AddMembership(newMembership); // کارایی دیگر اینجا } }
در این مثال، در خط 14 کلاس MembershipService از رابط IMembershipRepository برای دسترسی به لایه دسترسی به داده استفاده میکند. سپس در خط 32 بهجای ایجاد مستقیم یک نمونه از کلاس DatabaseMembershipRepository، از رابط IMembershipRepository استفاده میشود. این به دلیلی است که هر زمان که نیاز به تغییر نحوه دسترسی به دادهها (مثلاً استفاده از یک سرویس دیگر به جای دیتابیس) داشتهباشید، لازم نباشد که تغییری در کد خط 32 صورت گیرد.
ورود به سایت