SOLID設計原則-筆記

物件導向系列

單一職責原則 Single Responsibility Principle (SRP)

最早的解釋是:一個模組應該只有一個改變的理由。

到整潔架構的定義是:一個模組只對一個角色負責。

來看看以下程式碼的設計

public interface IUserService
{
   bool Login(string userName, string password);
   bool Register(string email, string userName, string password, string confirmPasword);
   void LogError(Exception exception, string errorMessage);
   void SendEmail(string emailAddress, string content);
}

以上IUserService介面包含三個不同職責,LogError和SendEmail不應該屬於IUserService職責。

上述會有若是類別集中會有以下的問題

  1. 程式和類別複雜度大幅提升
  2. 當一個程式碼有重複利用需求,可以考慮SRP原則,要了解需求、來定義一個物件責任

改成如下

public interface IUserService
{
    bool Login(string userName, string password);
    bool Register(string email, string userName, string password, string confirmPasword);
}

public interface IErrorLogger
{
    void Log(Exception exception, string errorMessage);
}

public interface IEmailSender
{
    void Send(string emailAddress, string content);
}

開放封閉原則 Open Closed Principle (OCP)

解釋:一個軟體應該對於擴展是開放,對修改是關閉。

這段白話解釋是應該藉由新增功能來擴充功能,而不是修改程式碼。

筆者在看大話重構有看到關於OCP的原則,詳細看這一篇筆者筆記https://www.dotblogs.com.tw/bda605/2022/03/08/223126

實現OCP的手段可以透過

  1. 透過繼承來擴充
  2. 使用擴充方法來擴充既有類別
  3. 利用抽象型別,來多載物件

以下例子

public class ReportGeneration
{
    private string _reportType;
    public ReportGeneration(string reportType) 
    {
       _reportType = reportType;
    }
    
    public void GenerateReport(Employee employee)
    {
        if (ReportType == "CRS")
        {
            // Report generation with employee data in Crystal Report.
        }
        if (ReportType == "PDF")
        {
            // Report generation with employee data in PDF.
        }
    }
}

上面會遇到問題

  1. 如果要追加CSV的產出,會必須動到原本的 ReportGeneration 類別。
  2. 必須測試ReportGeneration類別所有功能,確保最新修改有無破壞現有功能。
// Interface
public interface IReportGeneration
{
    void GenerateReport(Employee employee);
}

// Clients
public class PdfReportGeneration : IReportGeneration
{
    public void GenerateReport(Employee employee)
    {
        Console.WriteLine("Generating pdf report.");
    }
}

public class CRSReportGeneration : IReportGeneration
{
    public void GenerateReport(Employee employee)
    {
        Console.WriteLine("Generating CRS report.");
    }
}

public class CsvReportGeneration : IReportGeneration
{
    public void GenerateReport(Employee employee)
    {
        Console.WriteLine("Generating csv report.");
    }
    
}

里氏替換原則 Liskov Substitution Principle (LSP)

定義:子類別可以替換其父類別。

目前C#裡面可以用抽象類別方式搭配virtual和override,程式看到老爸的類別有virtaul才會執行子類別有override的方法。

筆者比較常用LSP搭配Interface來實現,因為用抽象類別或類別繼承要寫好。

// Interface
internal interface IBird
{
   void MakeSound();

   void Fly();

   void Run();
}

// Implementations
public class Duck : IBird
{
   public void MakeSound()
   {
       Console.WriteLine("Making sound.");
   }

   public void Fly()
   {
       Console.WriteLine("Flying...");
   }

   public void Run()
   {
       Console.WriteLine("Running...");
   }
}

// This breaks the Liskov substituion principle because if we follow polymorphism and call the Fly() method from IBird reference variable
// then it will throw NotImplementedException.
public class Ostrich : IBird
{
   public void MakeSound()
   {
       Console.WriteLine("Making sound.");
   }

   // Ostrich cannot fly.
   public void Fly()
   {
       throw new NotImplementedException();
   }

   public void Run()
   {
       Console.WriteLine("Running...");
   }
}

上述有個問題是若呼叫Fly(),它會拋出異常。

改成以下

// Interfaces
internal interface IBird
{
    void MakeSound();

    void Run();
}

internal interface IFlyingBird : IBird
{
    void Fly();
}

public class Duck : IFlyingBird
{
    public void MakeSound()
    {
        Console.WriteLine("Making sound.");
    }

    public void Fly()
    {
        Console.WriteLine("Flying...");
    }

    public void Run()
    {
        Console.WriteLine("Running...");
    }
}

public class Ostrich : IBird
{
    public void MakeSound()
    {
        Console.WriteLine("Making sound.");
    }

    public void Run()
    {
        Console.WriteLine("Running...");
    }
}

介面隔離原則 Interface Segregation Principle (ISP)

定義:客戶不應該強迫相依沒有使用的方法。

Interface有多個方法,應該拆成多個介面,而介面設計特定需求來定義規格,用多個介面來取代單一大型介面,將特定功能規格定義專屬小介面上。

public interface IEmployeeTasks
{
   void CreateTask();
   void AssginTask();
   void WorkOnTask();
}

public class TeamLead : IEmployeeTasks
{
   public void CreateTask()
   {
       Console.WriteLine("Task created.");
   }

   public void AssginTask()
   {
       Console.WriteLine("Task assigned.");
   }

   public void WorkOnTask()
   {
       Console.WriteLine("Started working on the task.");
   }
}

public class Manager : IEmployeeTasks
{
   public void CreateTask()
   {
       Console.WriteLine("Task created.");
   }

   public void AssginTask()
   {
       Console.WriteLine("Task assigned.");
   }

   // The Manager client has been forced to implement `WorkOnTask()` method although Manager does not work on task.
   public void WorkOnTask()
   {
       throw new NotImplementedException();
   }
}

public class Programmer : IEmployeeTasks
{
   // The Programmer client has been forced to implement `CreateTask()` method although Programmer cannot create task.
   public void CreateTask()
   {
       throw new NotImplementedException();
   }

   // The Programmer client has been forced to implement `AssginTask()` method although Programmer cannot assign task.
   public void AssginTask()
   {
       throw new NotImplementedException();
   }

   public void WorkOnTask()
   {
       Console.WriteLine("Started working on the task.");
   }
}

上述問題造成

  1. 管理用戶端已經強制實現方法,管理者不處理WorkOnTask()
  2. 程式設計師被迫實現AssignTask(),程式設計師無法建立任務和指派任務。CreateTask(),AssignTask()

以下程式碼解決上述問題

// Interfaces
public interface ILead
{
    void CreateTask();
    void AssginTask();
}

public interface IProgrammer
{
    void WorkOnTask();
}

// Clients
public class Manager : ILead
{
    public void CreateTask()
    {
        Console.WriteLine("Task created.");
    }

    public void AssginTask()
    {
        Console.WriteLine("Task assigned.");
    }
}

public class TeamLead : ILead, IProgrammer
{
    public void CreateTask()
    {
        Console.WriteLine("Task created.");
    }

    public void AssginTask()
    {
        Console.WriteLine("Task assigned.");
    }

    public void WorkOnTask()
    {
        Console.WriteLine("Started working on the task.");
    }
}

public class Programmer : IProgrammer
{
    public void WorkOnTask()
    {
        Console.WriteLine("Started working on the task.");
    }
}

 

相依反轉原則 Dependency Inversion Principle (DIP)

定義:高層次模組不應該依賴低層次模組、兩者依賴於介面

高階模組呼叫

public class EmailSender
{
    public void Send(string email, string content)
    {
        Console.WriteLine($"Sending email to {email}");
    }
}

// High level module
public class OrderService
{
    private readonly EmailSender _emailSender;

    public OrderService(EmailSender emailSender)
    {
        _emailSender = emailSender;
    }

    public void CreateOrder()
    {
        Console.WriteLine("Creating order");

        //Sending order details
        _emailSender.Send("tanvirarjel2@gmail.com", "This is order detail.");
    }
}

上述程式碼:高級模組緊OrderService密依賴於低級模組EmailSender

如果我們反轉依賴並重新設計程式如下,它將符合依賴反轉原則

// Low level module
public class EmailSender : IEmailSender
{
    public void Send(string email, string content)
    {
        Console.WriteLine($"Sending email to {email}");
    }
}

// High level module

// Abstraction
public interface IEmailSender
{
    void Send(string email, string content);
}

public class OrderService
{
    private readonly IEmailSender _emailSender;

    public OrderService(IEmailSender emailSender)
    {
        _emailSender = emailSender;
    }

    public void CreateOrder()
    {
        Console.WriteLine("Creating order");

        //Sending order details
        _emailSender.Send("tanvirarjel2@gmail.com", "This is order detail.");
    }
}

參考資料

https://igouist.github.io/series/%E8%8F%9C%E9%9B%9E%E8%88%87%E7%89%A9%E4%BB%B6%E5%B0%8E%E5%90%91/--菜雞與物件導向 

https://yuanchieh.page/posts/2021/2021-06-15-youtube-%E7%9B%B4%E6%92%ADfred%E8%81%8A%E8%81%8Asolid%E8%A8%AD%E8%A8%88%E5%8E%9F%E5%89%87%E6%95%B4%E7%90%86/

 

 

 

 

 

元哥的筆記