Single-responsibility principle
یک کلاس فقط باید یک مسئولیت منفرد داشته باشد ، یعنی فقط تغییرات در یک قسمت از مشخصات نرم افزار باید بتواند بر مشخصات کلاس تأثیر بگذارد.
به عنوان مثال یک کلاس برای ثبت کاربر به صورت زیر تعریف میشود:
public class RegisterService
{
public void RegisterUser(string username)
{
if (username == "admin")
throw new InvalidOperationException();
SqlConnection connection = new SqlConnection();
connection.Open();
SqlCommand command = new SqlCommand("INSERT INTO [...]");
SmtpClient client = new SmtpClient("smtp.myhost.com");
client.Send(new MailMessage());
}
}
در مثال بالا اصل مسئولیت منفرد (Single-responsibility principle) رعایت نشده است به این دلیل که کلاس RegisterUser سه کار، ثبت کاربر، اتصال به پایگاه داده و ارسال ایمیل را انجام میدهد. برای تبعئیت از این اصل باید این کلاس را به سه کلاس خاص تقسیم کنیم که هر کدام یک کار واحد را انجام میدهند.
public void RegisterUser(string username)
{
if (username == "admin")
throw new InvalidOperationException();
_userRepository.Insert(...);
_emailService.Send(...);
}
Open-closed principle
قسمت باز برای گسترش (Open to Extension) به این معناست که باید کلاسها را طوری طراحی کرد، که بتوانید Functionality های جدید را با ظهور نیازمندیهای جدید به راحتی پیادهسازی کنیم. قسمت بسته برای تغییر (Closed for Modification) به این معناست که زمانی که یک کلاس را توسعه داده و کار آن را به اتمام رساندهایم، دیگر نباید نیاز به تغییر دادن آن داشته باشیم، مگر به منظور رفع کردن باگها.
public double Area(object[] shapes)
{
double area = 0;
foreach (var shape in shapes)
{
if (shape is Rectangle)
{
Rectangle rectangle = (Rectangle) shape;
area += rectangle.Width*rectangle.Height;
}
else
{
Circle circle = (Circle)shape;
area += circle.Radius * circle.Radius * Math.PI;
}
}
return area;
}
public class AreaCalculator
{
public double Area(Rectangle[] shapes)
{
double area = 0;
foreach (var shape in shapes)
{
area += shape.Width*shape.Height;
}
return area;
}
}
این مثال از اصل Open-closed principle تبعیت نمیکند به این دلیل که متد Area برای گسترش باز نیست همچنین فقط می تواند شکلهای مستطیل و دایره را در بر بگیرد و در صورتی که بخواهیم مثلا شکل مثلث نیز در آن گنجانده شود باید متد Area را دست کاری کنیم پس برای تغییر باز نیست. با اضافه کردن کلاس Shape و ارثبری از آن این مشکل حل می شود.
public abstract class Shape
{
public abstract double Area();
}
public class Rectangle : Shape
{
public double Width { get; set; }
public double Height { get; set; }
public override double Area()
{
return Width*Height;
}
}
public class Circle : Shape
{
public double Radius { get; set; }
public override double Area()
{
return Radius*Radius*Math.PI;
}
}
public double Area(Shape[] shapes)
{
double area = 0;
foreach (var shape in shapes)
{
area += shape.Area();
}
return area;
}
Liskov substitution principle
آبجکتهای کلاسهای والد و فرزند، باید بتوانند بدون به وجود آمدن هیچ مشکلی در کدها به جای هم استفاده شوند.
namespace SOLID_PRINCIPLES.LSP
{
class Program
{
static void Main(string[] args)
{
Apple apple = new Orange();
Debug.WriteLine(apple.GetColor());
}
}
public class Apple
{
public virtual string GetColor()
{
return "Red";
}
}
public class Orange : Apple
{
public override string GetColor()
{
return "Orange";
}
}
}
این مثال از اصل LSP تبعیت نمیکند به این دلیل که کلاس Orange بدون تغییر در خروجی برنامه نمیتواند جایگزین کلاس Apple شود. متد GetColor توسط کلاس Orange جایگزین نمیشود بنابراین نارنجی بودن یک سیب را بر میگرداند. برای تغییر در این ساختار یک کلاس به نام Fruit تعریف میکنیم که کلاسهای Apple و Orange از آن ارثبری میکنند.
namespace SOLID_PRINCIPLES.LSP
{
class Program
{
static void Main(string[] args)
{
Fruit fruit = new Orange();
Debug.WriteLine(fruit.GetColor());
fruit = new Apple();
Debug.WriteLine(fruit.GetColor());
}
}
public abstract class Fruit
{
public abstract string GetColor();
}
public class Apple : Fruit
{
public override string GetColor()
{
return "Red";
}
}
public class Orange : Fruit
{
public override string GetColor()
{
return "Orange";
}
}
}
Interface segregation principle
برای استفاده از اینترفیس ها آنها را باید به اجزای کوچکتری تقسیم کرد. وقتی یک کلاس از یک اینترفیس بزرگ استفاده میکند ممکن است برخی از این متد ها در کلاس مورد نظر قابل استفاده نباشند. اما وقتی یک اینترفیس بزرگ به چند اینترفیس کوچک تقسیم می شود هر کلاس میتواند در صورتی که به اینترفیس خاصی نیاز داشت از آن استفاده نماید. با این امکان اگرچه تعداد اینترفیس ها بیشتر می شوند و ممکن است تکرار رخ دهد اما به دلیل اینکه منطق برنامه ما در اینترفیس ها اجرا نمی شود میتوان این مسئله را نادیده گرفت.
public interface IWorker
{
string ID { get; set; }
string Name { get; set; }
string Email { get; set; }
float MonthlySalary { get; set; }
float OtherBenefits { get; set; }
float HourlyRate { get; set; }
float HoursInMonth { get; set; }
float CalculateNetSalary();
float CalculateWorkedSalary();
}
public class FullTimeEmployee : IWorker
{
public string ID { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public float MonthlySalary { get; set; }
public float OtherBenefits { get; set; }
public float HourlyRate { get; set; }
public float HoursInMonth { get; set; }
public float CalculateNetSalary() => MonthlySalary + OtherBenefits;
public float CalculateWorkedSalary() => throw new NotImplementedException();
}
public class ContractEmployee : IWorker
{
public string ID { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public float MonthlySalary { get; set; }
public float OtherBenefits { get; set; }
public float HourlyRate { get; set; }
public float HoursInMonth { get; set; }
public float CalculateNetSalary() => throw new NotImplementedException();
public float CalculateWorkedSalary() => HourlyRate * HoursInMonth;
}
این برنامه از اصل ISP تبعیت نمی کند به این دلیل که کلاس FullTimeEmployee نیازی به تابع CalculateWorkedSalary() ندارد و کلاس ContractEmployee نیازی به تابع CalculateNetSalary() ندارد. هیچ یک از این متدها هدف اصلی کلاس را پیش نمیبرند و فقط چون از کلاس IWorker ارث میبرند، تعریف میشوند. برنامه زیر با تبعیت از اصل ISP بهینه شده است.
public interface IBaseWorker
{
string ID { get; set; }
string Name { get; set; }
string Email { get; set; }
}
public interface IFullTimeWorkerSalary : IBaseWorker
{
float MonthlySalary { get; set; }
float OtherBenefits { get; set; }
float CalculateNetSalary();
}
public interface IContractWorkerSalary : IBaseWorker
{
float HourlyRate { get; set; }
float HoursInMonth { get; set; }
float CalculateWorkedSalary();
}
public class FullTimeEmployeeFixed : IFullTimeWorkerSalary
{
public string ID { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public float MonthlySalary { get; set; }
public float OtherBenefits { get; set; }
public float CalculateNetSalary() => MonthlySalary + OtherBenefits;
}
public class ContractEmployeeFixed : IContractWorkerSalary
{
public string ID { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public float HourlyRate { get; set; }
public float HoursInMonth { get; set; }
public float CalculateWorkedSalary() => HourlyRate * HoursInMonth;
}
Dependency inversion principle
اگر کلاس خاصی(high-level) که از کلاس های دیگر(low-level) استفاده می کند وابستگی مستقیمی با کلاس های low-level داشته باشد سبب بروز این مشکل خواهد شد که اگر کلاس low-level دیگری به مجموعه افزوده شود اجبارا کلاس high-level نیز بایستی تغییر کند.
public interface ICustomerDataAccess
{
string GetCustomerName(int id);
}
public class CustomerDataAccess: ICustomerDataAccess
{
public CustomerDataAccess() {}
public string GetCustomerName(int id)
{
return "Customer Name";
}
}
public class DataAccessFactory
{
public static ICustomerDataAccess GetCustomerDataAccessObj()
{
return new CustomerDataAccess();
}
}
public class CustomerBusinessLogic
{
ICustomerDataAccess _custDataAccess;
public CustomerBusinessLogic()
{
_custDataAccess = DataAccessFactory.GetCustomerDataAccessObj();
}
public string GetCustomerName(int id)
{
return _custDataAccess.GetCustomerName(id);
}
}