
اصول پنجگانه ی S.O.L.I.D یک سری قواعد برنامه نویسی هستند که رعایت آنها باعث خوانایی بهتر کد، افزایش قابلیت توسعه پذیری و نگهداری آن می شود. ابتدای نام هر یک از این اصول، تشکیل کلمه ی SOLID را می دهد. این اصول عبارت اند از:
- کاراکتر S برای Single Responsibility Principle
- کاراکتر O برای Open/Closed Principle
- کاراکتر L برای Liskov Substitution Principle
- کاراکتر I برای Interface Segregation Principle
- کاراکتر D برای Dependency Inversion Principle
در ادامه هرکدام از این اصول بررسی خواهند شد.
1. Single Responsibility Principle (اصل تک وظیفه ای)
طبق این اصل، هر کلاس تنها باید یک کار را انجام دهد. به متد زیر دقت کنید.
1 2 3 4 5 6 7 8 9 10 11 |
public void Add(Student student) { try { //add student to database } catch(Exception e) { System.IO.File.WriteAllText(@"D:\\errors.txt", e.Message.ToString()); } } |
درست است که این کد کار می کند اما یک مشکل اساسی دارد. متد Add ، تنها وظیفه ی افزودن را بر عهده دارد، اما وظیفه ی ثبت خطا در فایل متنی را هم برعهده گرفته است، با توجه به قواعد SOLID باید این وظیفه را به کلاس دیگری سپرد.
1 2 3 4 5 6 7 |
public class FileLogger { public static void Log(string content) { System.IO.File.WriteAllText(@"D:\\errors.txt", content); } } |
حال می توان متد Add را اینگونه نوشت.
1 2 3 4 5 6 7 8 9 10 11 |
public void Add(Student student) { try { //add student to database } catch(Exception e) { FileLogger.Log(e.Message); } } |
2. Open / Closed Principle
منظور از OCP این است که برنامه نویس باید کلاس ها و اشیاء را طوری ایجاد نماید که همواره امکان گسترش (Extend) آن وجود داشته باشد اما برای گسترش نیازی به تغییر در اصل کلاس نباشد.یعنی اینکه کدها و کلاس ها بایستی همواره برای گسترش و توسعه باز باشند اما برای ویرایش و تغییر بسته باشند. البته منظور از بسته بودن برای ویرایش این است که نیازی به تغییر کد ها نباشد.
در مثال زیر قرار است که با توجه به محصولات گوناگون، تخفیف های مختلفی لحاظ شود:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public class Product { public string Name { get; set; } public int Price { get; set; } public int ProductType { 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; } } |
اگر بعدا ProductType جدیدی اضافه شود، مجبور به ویرایش کلاس هستیم. با پیروی از اصل Open/Close از این مشکل جلوگیری می کنیم. بدین شکل که متد GetDiscount را به صورت Virtual پیادهسازی کرده و اشکال مختلف آن را در قالب کلاس هایی که مشتق شده اند به صورت Override پیادهسازی می کنیم:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
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) * 5; } } public class ProductType2 : Product { public override double GetDiscount() { return (price / 100) * 10; } } public class ProductType3 : Product { public override double GetDiscount() { return (price / 100) * 15; } } |
نحوه ی استفاده از آن به شکل زیر است:
1 2 3 4 |
ProductType1 p1 = new ProductType1(); p1.name = "Mobile"; p1.price = 1200; Console.WriteLine($"{p1.name} costs {p1.price} and its discount is {p1.GetDiscount()}"); |
3. Liskov Substitution Principle (جایگزینی لیسکوف)
اصل LSP میگوید: “زیر کلاسها باید بتوانند جایگزین کلاس پایهی خود باشند”. یعنی باید بتوان نمونه های زیرکلاس ها را به جای کلاس پدر به کار برد و بدون آن که برنامه به دستکاری یا تغییر نیاز داشته باشد باید بتواند مانند گذشته کار کند.
مثال:
در هندسه، ما مستطیل را یک کلاس پایه برای مربع میدانیم. به کد زیر توجه کنید :
1 2 3 4 5 6 7 8 9 10 |
public class Rectangle { public virtual int Width {get; set;} public virtual int Height {get; set;} } public class Square : Rectangle { //codes specific to square will be added } |
و می توان گفت:
1 2 3 |
Rectangle o = new Rectangle(); o.width=5; o.height=6; |
با توجه به LSP باید بتوانیم مستطیل را با مربع جایگزین کنیم:
1 2 3 |
Rectangle o = new Square(); o.width=5; o.height=6; |
چه شد؟ مگر مربع میتواند طول و عرض نا برابر داشته باشد؟! بدیهی است که نمی تواند. خوب این به چه معنی است؟ به این معنی که ما نمیتوانیم کلاس پایه را با کلاس مشتق شده جایگزین کنیم و باز هم این معنی را میدهد که ما اصل LSP را نقض کرده ایم. در ادامه راه حل را بررسی می کنیم.
راه حل:
یک کلاس انتزاعی (abstract) را به شکل زیر ایجاد و سپس دوکلاس Square و Rectangle را از آن مشتق میکنیم :
1 2 3 4 5 |
public class Shape { public virtual int Width { get; set; } public virtual int Height { get; set; } } |
الان دو کلاس مستقل از هم داریم. یکی Square و دیگری Rectangle که هر دو از کلاس Shape مشتق شده اند. به پیاده سازی کلاس های مستطیل و مربع که در زیر آمده است دقت کنید:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
//rectangle class public class Rectangle : Shape { public override int Width { get { return base.Width; } set { base.Width = value; } } public override int Height { get { return base.Height; } set { base.Height = value; } } } //square class public class Square : Shape { public override int Width { get { return base.Width; } set { base.Width = value; base.Height = value; } } public override int Height { get { return base.Height; } set { base.Width = value; base.Height = value; } } } |
نحوه ی استفاده از آن به شکل زیر است:
1 2 3 4 5 6 7 8 9 |
Shape r = new Rectangle(); r.Height = 10; r.Width = 15; Console.WriteLine($"Rectangle - Height={r.Height} Width={r.Width}"); Shape s = new Square(); s.Height = 20; s.Width = 25; Console.WriteLine($"Square - Height={s.Height} Width={s.Width}"); |
هنگام استفاده از کلاس مربع، هر عددی که به طول مربع بدهیم هم به طول و هم به عرض داده می شود. در مورد عرض هم قضیه به همین صورت است.
یک مثال دیگر در مورد اصل Liskov
به کد زیر دقت کنید، کلاس والدی به نام Product وجود دارد که دارای پارامتری از نوع Virtual به نام Discount است. دو کلاس Mobile و Tablet از این کلاس مشتق شده اند و هر کدام به شیوه ی خودشان پارامتر Discount را Override کرده اند. چون در این مثال اصل LSP رعایت شده است کلاس های Mobile و Tablet می توانند به جای کلاس والد خود یعنی Product به کار روند.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
public class Product { public int Id { get; set; } public string Name { get; set; } public int Price { get; set; } public virtual int Discount { get; } } public class Mobile : Product { public override int Discount { get => base.Price - (base.Price / 100 * 25); } } public class Tablet : Product { public override int Discount { get => base.Price - (base.Price / 100 * 15); } } |
به نحوه ی استفاده از آن دقت کنید:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
static void Main(string[] args) { Product mobile = new Mobile(); mobile.Id = 101; mobile.Name = "Samsung"; mobile.Price = 2000; Console.WriteLine($"{mobile.Id}- {mobile.Name} - Price: {mobile.Price} - Discount: {mobile.Discount}"); Product tablet = new Tablet(); tablet.Id = 104; tablet.Name = "IPod"; tablet.Price = 4600; Console.WriteLine($"{tablet.Id}- {tablet.Name} - Price: {tablet.Price} - Discount: {tablet.Discount}"); } |
4. Interface Segregation Principle (تفکیک اینترفیس ها)
طبق اصل ISP کلاینتها نباید وابسته به متدهایی باشند که آنها را پیادهسازی نمیکنند.
تنها متدهایی باید در اینترفیس نوشته شوند که در همه جای برنامه (هنگام استفاده از اینترفیس) کاربرد دارند. اگر قرار باشد که متدی فقط در یک جا استفاده شود نباید در اینترفیس اصلی نوشته شود چون در این صورت تمام قسمت های برنامه مجبور به پیادهسازی آن هستند و این خوب نیست. برای حل این مشکل، آن متدی که فقط در بعضی جاها استفاده می شود را در یک اینترفیس تعریف می کنیم و خود اینترفیس باید از اینترفیس اصلی ارث بری کند.
1 2 3 4 5 6 7 |
public interface IDatabaseManager { void Add(); void Remove(); void Update(); void RemoveAll(); } |
در کد بالا، متد RemoveAll فقط در یک جا استفاده می شود ولی تمام قسمت های برنامه باید آن را پیادهسازی کنند که این نقض قانون ISP است.برای حل این مشکل باید متد RemoveAll را از این اینترفیس حذف کنیم و آن را در اینترفیس جدیدی بنویسیم:
1 2 3 4 |
public interface IDatabaseRemoveAll : IDatabaseManager { void RemoveAll(); } |
حال، هر جایی از برنامه که به متد RemoveAll احتیاجی نباشد اینترفیس IDatabaseManager را پیادهسازی می کند و هر جایی که به RemoveAll احتیاج داشته باشد، اینترفیس IDatabaseRemoveAll را پیادهسازی می کند، در این صورت به متدهای تعریف شده در اینترفیس پدر هم دسترسی دارد.
5. Dependency Inversion Principle (وارونگی وابستگی)
اصل DIP به ما میگوید که: “ماژولهای سطح بالا نباید به ماژولهای سطح پایین وابسته باشند، هر دو باید به انتزاعات وابسته باشند. انتزاعات نباید وابسته به جزئیات باشند، بلکه جزئیات باید وابسته به انتزاعات باشند.”
به جای اینکه کلاینت به کلاس ها وابسته باشد باید به انتزاع ها وابسته باشد.
در مثال زیر، کلاس Database Manager عملیات های Add, Update, Remove را انجام می دهد و گزارش کار خود را از طریق Notification اعلام می کند. چندین نوع Notification وجود دارند مانند: پیامک و ایمیل و…
طبق اصل DIP، کلاس Database Manager به ماژول سطح پایینی مثل کلاس پیامک وابسته نیست بلکه به انتزاعی به نام اینترفیس Notification وابسته است. به کد زیر دقت کنید:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
public interface INotification { void Send(string message); } public class EmailNotification : INotification { public void Send(string message) { Console.WriteLine($"{message}.\nNotification is sent via Email"); } } public class SMSNotification : INotification { public void Send(string message) { Console.WriteLine($"{message}.\nNotification is sent via SMS"); } } |
کلاس Database Manager به شکل زیر است و فقط وابسته به انتزاع INotification است.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public class DatabaseManager { private INotification _notification; public DatabaseManager(INotification notification) { this._notification = notification; } public void Add() { _notification.Send("Add"); } public void Remove() { _notification.Send("Remove"); } public void Update() { _notification.Send("Update"); } } |
نحوه ی استفاده:
1 2 3 4 5 6 7 |
static void Main(string[] args) { DatabaseManager dm1 = new DatabaseManager(new EmailNotification()); dm1.Add(); DatabaseManager dm2 = new DatabaseManager(new SMSNotification()); dm2.Update(); } |
با اجرای پروژه، نتیجه ای به شکل زیر به نمایش درخواهد آمد:
Add
Notification is sent via Email
Update
Notification is sent via SMS