物件導向設計原則:SOLID

by NickChi
物件導向設計原則SOLID

在開始介紹前大家可以思考一下以下的開發情境

  • 開發的時間長? 還是維護的時間較長?
  • 有團隊一起開發? 還是一個人寫Code
  • 一個長期維護的專案,需求變更頻繁度?
  • 你如何讓程式碼具備可讀性與擴充性
  • 如何避免在修改程式的過程中引發連鎖反應?(改A壞B)

OOP的四個特性

  • 抽象(Abstraction)
    • 將真實世界的需求轉換成為OOP中的類別
    • 類別可以包含狀態(屬性)與行為(方法)。
  • 封裝(Encapsulation)
    • 隱藏/保護內部實作的細節,並可以對屬性或方法設定存取層級(Public,Private,Protected)
  • 繼承(Inheritance)
    • 可以讓您建立新類別以重複使用、擴充和修改其他類別中定義的行為
  • 多型(Polymorphism)
    • 在相同介面下,可以用不同的型別來實現。
    • 多型有分成好幾種不同類型。

程式設計的基本流程

第一步:
從需求或規格中進行”抽象化”的過程
透過”抽象化”過程定義出類別
第二步:
對實作的細節進行”封裝”(隱藏、保護)

第三步:
透過”繼承”來重複利用、擴充和修改基底類別的定義


透過”繼承”來重複利用、擴充和修改基底類別的定義

class BaseClass
{
  public string Name{get;set;}
  public int Age {get;set;}
  public virtual void Output()=>Console.WriteLine("Hello");
  public BaseClass(string name) => Name =name;
  public BaseClass()=> Name ="";
}
class DerivedClass : BaseClass
{
    public string Department { get; set; }
    public DerivedClass() : base("Default") 
    {
        base.Age = 18;
        //base.Department = "IT";
        base.Output();
        this.Age = 19;
        this.Department ="IT";
        this.Output();
    }
    public override void Output()=>Console.WriteLine("Hello !!");
}

在C#中所有類都是”多型”

  • 在設計時期(Design Time)
    • 基底類別可以定義和實作【虛擬】屬性或方法(virtual)
    • 衍生類別可以【覆寫】這些虛擬的屬性或方法(override)
  • 在執行時期(Runtime)
    • 當呼叫基底類別的虛擬方法時,會改呼叫子類別覆蓋的方法
  • 在C#中,所有類型都是多型類型
    • 因為所有類型(包誇使用者定義的類型)都是繼承自Object
  • 如果再C#中設計防止衍生類別覆蓋虛擬成員
    • public sealed override void Dowork(){}
  • 多載(Overloading)比較有點爭議(有些人認為這不算多型)

內聚力與耦合力(Cohesion & Coupling)

何謂”模組”(Module)

  • 一個抽象的概念
  • 以C#舉例
    • 可能是一個類別(Class)
    • 可能是一個方法(method)
    • 可能是一個組件(assembly)

內聚力 Cohesion

什麼是內聚力?一次專注做一件事情,這件事情做得越好他的內聚力就越高

一個模組內完成一件工作的度量指標

高內聚力

  • 在一個模組內只完成一件工作
  • 內聚力高,意味者該模組可以獨立運作,也意味者更容易重複利用
  • 範例:一個Class只負責一件事情(例如寄送郵件)

低內聚力

  • 在一個模組內完成多份工作
  • 內聚力低,意味者這個模組會造成難以維護/測試/重用/理解
  • 範例:所有功能寫在一個class裡面或一個method有500行程式碼

最佳實務

  • 在設計模組的時候,要盡量設計出高內聚力的程式碼。
  • 若要在一個模組內完成多項工作,建議拆成多個不同的類別
  • 實現SRP就是實現提高內聚力的一種表現

耦合力 Coupling

模組跟模組之間的關聯強度

  • 模組之間互相依賴的程度
  • 衡量兩個模組的緊密連接程度
  • 範例:在ClassB裡面,直接建立了ClassA的物件實體,就會建立ClassA與ClassB之間的耦合關係

高耦合力

  • 意味者當改了A模組時,相關聯的B模組就會容易被影響(改A壞B)

低耦合力

  • 當修改模組的時候,有越少的模組被影響,就意味者耦合力較低

最佳實務

  • 在設計不同模組的時候,要盡量設計出低耦合力的程式碼。
  • 實現DIP就是實現降低耦合力的一個原則

隨堂測驗


跟多少型別發生耦合?

public  class InvitaionService
{
    public void SendInvite(string email,string firstName,string lastName)
    {
        if(String.IsNullOrWhiteSpace(firstName))||(String.IsNullOrWhiteSpace(lastName))
        {
            throw new Exception("Name is not vaild");
        }
    }
    if(!email.Contain("@")||email.Contains("."))
    {
        throw new Exception("Email is not vaild!");
    }
    SmtpClient client = new SmtpClient();
    client.Send(new MailMessage("mysite@google.com,email"))
    {
        Subject ="Please join me at my party!"
    }

}

Ans: string、Exception、SmtpClient、MailMessage

設計出一個好的程式

內聚力越高越好、耦合度越低越好
高內聚、低耦合

隨堂測驗

串聯的耦合關係

public class Order
{
    private ShoppingCartContents cart;
    private float salesTax;
    public Order(ShoppingCartContents cart,float salesTax)
    {
        this.cart=cart;
        this.salesTax=salesTax;
    }
    public float OrderTotal()
    {
        float cartTotal = 0;
        for(int i=0;i<cart.item.length;i++)
        {
            cart.item+=cart.item[i].price;
        }
        return cart.item;
    }
}
public class ShoppingCartContents
{
    public ShoppingCart[] items;
}
public class ShoppingCart
{
    public float Price;
    public int Quanity;
}

完美上來看要寫出低耦合高內聚的程式碼,但現實沒有這麼簡單,所以我們才需要一些原則來幫助我們釐清什麼樣的程式才是好的

介紹SOLID物件導向設計原則

何謂原則(Principle)

  • A principle is a concept or value that is a guide for behavior or evaluation
  • 所謂【原則】(Principle)就是一種【概念】或【價值】,用來導引你產生適切的行為價值評量方法

白話文解釋

  • 依循SOLID原則,可以寫出比較好的程式碼
  • 依循SOLID原則,能夠判斷程式碼的好壞

背起來

  • 單一責任原則SRP(Single Responsibility Principle)
  • 開放封閉原則OCP(Open Closed Principle)
  • 里氏替換原則LSP(Liskov Substitution Principle)
  • 介面隔離原則ISP(Interface Segregation Principle)
  • 相依反轉原則DIP(Dependence Inversion Principle)

學習SOLID物件導向設計原則的好處

  • 降低程式碼複雜程度
  • 具有較佳程式碼可讀性
  • 提升模組可重複利用性
  • 讓模組具有高內聚,低耦合力
  • 面臨變更需求時可減少破壞現有模組的風險

單一責任原則SRP

何謂責任(Responsibility)

  • 責任= reason to change (改變的理由)
  • 當一個類別擁有多個不同的責任,意味者一個類別責任多項不同的工作,當需求變更時,更動一個類別的理由也可能不只一個

以下類別有多少責任?

public class OrderManger
{
    public bool LoadOrder()
    {
        //1.建立資料庫連線(包含寫死的連線字串)
        //2.執行ADO.NET資料存取(包含資料塞選)
        //3.跑回圈取得資料(包含資料格式轉換)
        //4.回傳資料
    }

}

思考:有什麼理由會需要改動到這個class
Ans:資料格式變了,連線字串改變,塞選資料條件變了

關於SRP的基本精神

  • 一個類別負擔太多責任時,意味者該類別可以被切割
    • 可以透過定義一個全新的類別輕鬆做到
    • 對類別進行適度的切割,方便日後管理與維護
  • SRP主要精神就是提高內聚力
    • 高內聚力意味者可以想到一個清楚的理由去改它!

低內聚力的示意圖

SRP主要精神就是在提高內聚力

常見的設計問題

  • 將所有功能寫在一個類別中
  • 類別複雜度過高
  • 維護時經常找不到應該要改哪裡
  • 發生邏輯問題時找不到BUG在哪裡
  • 使用類別時不知道應該呼叫哪個方法

關於SRP的使用時機

  • 兩個責任會在不同時間點產生變更需求
    • 當你想改資料庫查詢語法與修改系統紀錄的邏輯時,都會改到同一個類別,那就需要拆開!
  • 類別中有一段程式碼有重複利用的需求
    • 這段程式碼在其他類別也用的到
  • 系統中有個非必要的功能(未來需求),老闆又逼你要實作時
    • 責任會直接依附在類別中,但對維護造成困難

修改前:
請問他有SRP問題嗎,有的話 要如何重構

void Main()
{
    DataAccess.InsertData();
}
class DataAccess
{
    public static void InsertData()
    {
        Console.WriteLine("Data inserted into database successfully");
        Console.WriteLine("Logged Time" + DateTime.Now.ToLongTimeString()+"Log Data insertion completed successfully");
    }
}

修改後:
將寫入資料、寫LOG拆開

class DataAccess
{
    public static void InsertData()
    {
        Console.WriteLine("Data inserted into database successfully");
        Logger.Writelog();
    }
}
class Logger
{
    public static void WriteLog()
    {
        Console.WriteLine("Logged Time" + DateTime.Now.ToLongTimeString()+"Log Data insertion completed successfully");
    }
}

SRP討論事項

  • 你怎樣確認一個類別被賦予了過多的責任?
  • 套用SRP可能有副作用,因為類別變多導致耦合力增加

提高耦合力

意味者”改B壞A”的機會大幅增加!

關於SRP還需要注意的事

  • 參考YAGNI(You Ain’t Gonna Need It)原則
    • 不用急於在第一時間就專注於分離責任
    • 尚未出現的需求(未來的需求)不需要預先分離責任
    • 當需求變更的時候,再進行類別分割即可!
  • SRP是SOLID中簡單的,但卻是最難做到的
    • 需要不斷提升你的開發經驗重構技術
    • 如果沒有足夠的經驗去定義一個物件的Reponsibility那麼建議你不要過早進行SRP規劃!

練習情境

  • 請試者找出OrderMannger類別,不符合 單一責任原則地方
  • 請指出這個類別是否違反了SRP原則?
  • 請說明理由與如何改善?
public class OrderManaer
{
    public List<Product> products=new List<Product>();
    public void Processing()
    {
        //1.檢查商品庫存數量是否足夠
        //2.進行付款處理程序
        //3.進行送貨處理程序
    }
}

請試者修正該OrderManager類別,使其符合 單一責任原則
將多個責任使用新類別分離出來,但還有什麼問題?

public class Product{}
public class Cstomer{}
public class Stock
{
    public void checkAvailability(
        IEnumerable<Product> products){}
}
public class Payment
{
    public void Processing(
        Customer customer,
        IEnumerable<Product> product){}
}
public class Shipment
{
    public void SendProducts(
        Customer customer,
        IEnumerable<Product> products){}
}
public class OrderManger
{
    public List<Product> Products=new List<Product>();
    public Customer Customer{get;set;}
    public OrderManger()
    {

    }
    public void Processing()
    {
        new Stock().CheckAvailability(Products);
        new Payment().Processing(Customer,Products);
        new Shipment().SendProducts(Customer,Products);
    }
}

若客戶想要增加Lie Pay 付款方法,要改多少Code?
目前會有高耦合的問題

開放封閉原則OCP

  • Software entities(classes,modules,functions,etc.) should be open for extension but closed for modification
  • 軟體實體(類別、模組、函式等)應能開放擴充但封閉修改
  • 藉由增加新的程式碼來擴充系統的功能,而不是藉由修改原本已經存在的程式碼來擴充系統

關於OCP的基本精神

  • 一個類別需要開放,意味者該類別可以被擴充!
    • 可以透過繼承輕鬆做到
    • C#還有擴充方法可以輕鬆擴充既有類別
  • 一個類別需要封閉,意味者有其他人正在使用這個類別!
    • 如果程式已經編譯,但又已經有人在使用原本的類別
    • 封閉修改可以有效避免未知的問題發生

常見的設計問題

耦合力過高,擴充不易

關於OCP的實作方式

  • 採用分離與相依的技巧(相依於抽象)
    • 缺點:需要針對原有程式碼進行重構

關於OCP的C#範例
透過抽象類別限制其修改,並透過繼承開放擴充不同實作

關於OCP的使用時機

  • 你既有的類別已經被清楚定義,處於一個強調穩定的狀態
  • 需要擴充現有類別,加入新需求的屬性或方法
  • 擔心修改現有程式碼會破壞現有系統的運作
  • 系統剛開始設計時就決定採用OCP模式
    • 可以透過介面抽象類別進行實作

OCP討論事項

  • 當您剛接受維護一份2年前的程式碼,你會怎樣做?
    • 修改之前寫過的類別?
    • 擴充之前寫過的類別?
    • 直接修改舊有原始碼,會有哪些風險存在呢?
  • 如何讓系統在擴充需求時更簡單、更容易、更安全?
  • C#可以透過interface實踐OCP原則嗎?如何做到?

如何進行抽象化設計?多少人用過C#抽象類別?

Q&A:
抽象類別跟Interface差別
抽象類別 =>可以包含實作(耦合度增加)

練習情境

試者透過OCP原則重構程式碼
若客戶想要增加Log輸出到檔案的功能,你會如何改寫程式碼?

public class AppEvent
{
    public void GenerateEvent(string message)
    {
        Logger fooLogger  =new Logger();
        fooLogger.Log(message);
    }
}
public class Logger
{
    public void Log(string message)
    {
        Console.WriteLine(message);
    }
}

沒學過SOLID的開發者,可能會這樣寫

public class AppEvent
{
    public void GenerateEvent(string message)
    {
        Logger fooLogger  =new Logger();
        fooLogger.Log(message);
    }
}
public class Logger
{
    private readonly string _Target;
    public Logger(string target){_Target =target;}
    public void Log(string message)
    {
        if(_Targer == "Console")
            Console.WriteLine(message);
        else if(_Target == "File")
            File.WriteAllText("MyLog",message);
        else
            throw new NotImplementedException();
    }
}

此時,若又想要增加訊息傳送到遠端Web API或Storage呢?


  • 採用分離與相依的技巧
public interface ILogger
{
    void Log(string message);
}
public class ConsoleLogger:ILogger
{
    public void Log(string message)
    {
        Console.WriteLine(message);
    }
}
public class FileLogger:ILogger
{
    public void Log(string message)
    {
        File.WriteAllText("MyLog",message)
    }
}
public class AppEvent
{
    private readlony ILogger _Logger;
    public AppEvent(string loggerType)
    {
        this._logger = LoggerFactory.CreateLogger(loggerType);
    }
    public void GenerateEvent(string message)
    {
        _Logger.Log(message);
    }
}
public class LoggerFactory
{
    public static ILogger CreateLogger(string loggerType)
    {
        if(loggerType =="Console")
            return new ConsoleLogger();
        else if(loggerType=="File")
            return new FileLogger();
        else throw new NotImplementedException();

    }
}

如果要新增一個log,要新增一個class繼承Ilogger 在LoggerFactory增加

里氏替換原則LSP

  • Subtypes must be substitutable for their base types.
    • subtypes(衍生類別) = 類別
    • base types(基底類別) =介面、抽象類別、基底類別
  • 子型別必須可替換為他的基底型別
  • 如果你的程式有採用繼承介面,然後建立出幾個不同的衍生型別(Subtype)。在你的系統中只要是基底型別出現的地方,都可以用子型別來取代,而不會破壞程式原有的行為。

關於LSP的基本精神

  • 當實作繼承時,必須確保型別轉換後還能得到正確的結果
    • 當每個衍生類別都可以正確地替換為基底類別,且程式在執行時不會有異常的情況(如發生執行時期例外)
    • 必須正確的實作繼承多型

常見的設計問題

  • 不正確的實作繼承與多型
    • 第一版:沒有繼承,單純的計算矩形面積
    • 第二版:新增需求,增加Square類別(套用OCP原則)
    • 第三版:重構程式,正確套用LSP原則
  • 實作繼承時,在特定情況下發生執行時期錯誤(Runtime Error)
    • 範例程式
    • 違反LSP原則有時候較難發現

第一版:

void Main()
{
    Rectangle o = new Rectangle();
    o.Width = 40;
    o.Height =50;
    LSPBehavior.GetArea(o).Dump();
}
public class Rectangle
{
    public int Height{get;set;}
    public int Width{get;set;}
}
public class LSPBehavior
{
    public static int GetArea(Rectangle s)
    {
        if(s.Width>20)
        {
            s.Width = 20;
        }
        return s.Width * s.Height;
    }
}

第二版(OCP原則):

void Main()
{
    Square o = new Square();
    o.Width = 40;
    //o.Height =40;
    LSPBehavior.GetArea(o).Dump();
}
public class Rectangle
{
    public int Height{get;set;}
    public int Width{get;set;}
}
public class Square:Rectangle
{
    private int _height;
    private int _width;
    public int Height
    {
        get{return _height;}
        set{_height = _width =value;}
    }
    public int Width
    {
        get{return _width;}
        set{_width = _height =value;}
    }
}
public class LSPBehavior
{
    public static int GetArea(Rectangle s)
    {
        if(s.Width>20)
        {
            s.Width = 20;
        }
        return s.Width * s.Height;
    }
}

答案會是多少?

第二版不符合LSP

第三版(LSP):

void Main()
{
    Square o = new Square();
    o.Width = 40;
    //o.Height =40;
    LSPBehavior.GetArea(o).Dump();
}
public class Rectangle
{
    public virtual int Height{get;set;}
    public virtual int Width{get;set;}
}
public class Square:Rectangle
{
    private int _height;
    private int _width;
    public override int Height
    {
        get{return _height;}
        set{_height = _width =value;}
    }
    public override int Width
    {
        get{return _width;}
        set{_width = _height =value;}
    }
}
public class LSPBehavior
{
    public static int GetArea(Rectangle s)
    {
        if(s.Width>20)
        {
            s.Width = 20;
        }
        return s.Width * s.Height;
    }
}

善用virtual & override


第二個例子(不符合LSP)

class Customer
{
    public virtual double getDiscount(double TotalSales)
    {
        return TotalSales
    }
    public virtual void Add()
    {

    }
}
class GoldCustomer : Customer
{
    public override double getDiscount(double TotalSales)
    {
        return base.getDiscount(TotalSales) - 5;
    }
    public override void Add()
    {
        Console.WriteLine("GoldCustomer:Add");
    }
}
class SilverCustomer:Customer
{
    public overide double getDiscount(double TotalSales)
    {
        return base.getDiscount(TotalSales) - 5;
    }
    public override void Add()
    {
        Console.WriteLine("SilverCustomer:Add");
    }
}
class Enquiry:Customer
{
        public overide double getDiscount(double TotalSales)
    {
        return base.getDiscount(TotalSales) - 5;
    }
    public override void Add()
    {
        Console.WriteLine("Not allowed");
    }
}
void Main()
{
    List<Customer> Customers = new List<Customer)();
    Customer.Add(new SilverCustomer());
    Customer.Add(new GoldCustomer());
    Customer.Add(new Enquiry());

    foreach(Customer o in Customers)
    {
        o.Add();
    }
}

有沒有人這樣解決?

class Enquiry:Customer
{
        public overide double getDiscount(double TotalSales)
    {
        return base.getDiscount(TotalSales) - 5;
    }
    public override void Add()
    {

    }
}

修正(符合LSP)

interface IDiscount
{
    double getDiscount(double TotalSales);
}
interface IDatabase
{

}
class Customer:IDatabase,IDiscount
{
    public virtual double getDiscount(double TotalSales)
    {
        return TotalSales
    }
    public virtual void Add()
    {

    }
}
class GoldCustomer : Customer
{
    public override double getDiscount(double TotalSales)
    {
        return base.getDiscount(TotalSales) - 5;
    }
    public override void Add()
    {
        Console.WriteLine("GoldCustomer:Add");
    }
}
class SilverCustomer:Customer
{
    public overide double getDiscount(double TotalSales)
    {
        return base.getDiscount(TotalSales) - 5;
    }
    public override void Add()
    {
        Console.WriteLine("SilverCustomer:Add");
    }
}
class Enquiry:IDiscount
{
        public overide double getDiscount(double TotalSales)
    {
        return base.getDiscount(TotalSales) - 5;
    }
    public override void Add()
    {
        Console.WriteLine("Not allowed");
    }
}
void Main()
{
    List<Customer> Customers = new List<Customer)();
    Customer.Add(new SilverCustomer());
    Customer.Add(new GoldCustomer());
    Customer.Add(new Enquiry());

    foreach(Customer o in Customers)
    {
        o.Add();
    }
}

優點:這樣在編譯時期就可以看出錯誤

關於LSP的實作

  • 採用類別繼承方式來進行開發
    • 須注意繼承的實作方式
  • 採用合約設計方式來進行開發
    • 利用介面(interface)來定義基底型別(base type)

關於LSP的使用時機

  • 當你需要透過基底型別多型物件進行操作時

LSP討論事項

  • 在教導新人時,如何有效的避免繼承的錯誤實作?
  • 你會用抽象類別類別介面 來實現LSP原則? 為什麼?

介面隔離原則ISP

  • A:Many client specific interfaces are better than one general purpose interface.
  • B:Clients should not be forced to depend upon interface that they don’t use.
  • A:多個用戶端專用的介面優於一個通用需求介面
  • B:用戶端不應該強迫相依於沒用到的介面
  • 針對不同需求的用戶端,僅開放其對應需求的介面就好

關於ISP的基本精神

  • 把不同需求的屬性與方法,放在不同的介面中
    • 不要讓你的interface包山包海
    • 特定需求沒用到的地方,不要加到介面中,另外建一個
    • 可以拿interface當成群組來用(屬性與方法)
  • 使得系統可以更容易的達成鬆散耦合、安全重構、功能擴充

常見的設計問題

  • 將所有API需求都定義在一個超大介面中
  • 用戶端相依於一堆用不到得介面方法
    • 如果多個類別已經實作同一個胖介面
    • 就會導致某些別實作出用戶端用不到的方法
    • 這時應該可以拆分多的用戶端專用的介面進行實作
    • 所以一個實作介面的類別,不應該強迫去實作出這個類別不需要的方法(備註:這裡的不需要是指用戶端不需要)

關於ISP的使用時機

  • 當介面需要被分割的時候
  • 類別的使用時機可以被切割的時候
    • 假設類別有20個方法,並實作一個15個方法的介面
    • 有某個用戶端只會使用該類別中的10個方法
    • 你就可以為這類別的10個方法定義介面並設定實作介面
    • 你的用戶端就可以改用介面操作
    • 這個過程也可以用來降低主程式與這個類別的耦合力

有沒有符合ISP精神?

void Main()
{
    Console.Writenline("\n\nOpen Close Principle Demo ");
    DataProvider DataProviderObject = new SqlDataProvider();
    DataProviderObject.OpenProviderObject();
    DataProviderObject.ExcuteCommand();
    DataProviderObject.CloseConnection();
}
interface DataProvider
{
    int OpenConnection();
    int CloseConnection();
    int ExcuteCommand();
    int BeginTransation();
}
class SqlDataProvider:DataProvider
{
    public int OpenConnection()
    {
        Console.WriteLine("
        \nSql Connection opened successfully");
    }
    public int CloseConnection()
    {
        Console.WriteLine("
        Sql Connection Close successfully");
    }
    public int ExecuteCommand()
    {
        Console.WriteLine("
        Sql Command Executed successfully");
    }
    public int BeginTransaction()
    {
        Console.WriteLine("
        Sql BeginTransaction successfully");
    }
}

Q&A:沒有 BeginTransaction沒用到


修正版:

void Main()
{
    Console.Writenline("\n\nOpen Close Principle Demo ");
    DataProviderWithoutTransaction DataProviderObject = new SqlDataProvider();
    DataProviderObject.OpenConnection();
    DataProviderObject.ExcuteCommand();
    DataProviderObject.CloseConnection();
}
interface DataProviderWithoutTransaction
{
    int OpenConnection();
    int CloseConnection();
    int ExcuteCommand();
}
interface DataProvider:DataProviderWithoutTransaction
{
    int BeginTransaction();
}
class SqlDataProvider:DataProvider
{
    public int OpenConnection()
    {
        Console.WriteLine("
        \nSql Connection opened successfully");
    }
    public int CloseConnection()
    {
        Console.WriteLine("
        Sql Connection Close successfully");
    }
    public int ExecuteCommand()
    {
        Console.WriteLine("
        Sql Command Executed successfully");
    }
    public int BeginTransaction()
    {
        Console.WriteLine("
        Sql BeginTransaction successfully");
    }
}

ISP 討論事項

  • 你工作中是否有設計過超大介面的經驗?
  • 設計介面的時候,介面的大小應該如何判斷?如何群組?
  • 介面可以實作介面,使用的時機為何?

例子2

void Main()
{
    Console.WriteLine("\n\nOpen Close Principle Demo");
    IReadAndWrite Logger = new Logger();
    Logger.Write();
    Logger.Read();
}
interface IReadAndWrite
{
    string Read();
    void Write();
}
class Logger:IReadAndWrite
{
    public string Read(){return "";}
    public void Write(){}
}

需求變了 由用戶端來選擇要用的interface

void Main()
{
    Console.WriteLine("\n\nOpen Close Principle Demo");
    IWritable Logger = new Logger();
    Logger.Write();
    Logger.Read();
}
interface IReadable
{
     string Read();
}
interface IWritable
{
      void Write();
}
interface IReadAndWrite:IReadable,IWritable
{

}
class Logger:IReadAndWrite,IReadable,IWritable
{
    public string Read(){return "";}
    public void Write(){}
}

例子3
main 跟多少型別發生相依
Q&A:兩個

void Main()
{
    IDatabase cust = new Customer();
    cust.Add();
}
interface IDatebase
{
    void Add();
}
class Customer:IDatebase
{
    public void Add()
    {
        Console.WrtieLine("Add something");
    }
}

例子4
試者找出該IAllnOnceCar介面 不符合介面隔離則地方

public void Main()
{
    Driver o = new Driver();
    o.StartEngine();
    o.Drive();
    o.StopEngine();
}
public interface IAllInOneCar
{
    void StartEngine();
    void Drive();
    void StopEngine();
    void ChangeEngine();
}
public class Driver:IAllInOneCar
{
    public void ChangeEngine(){
    throw new NotImplementedException();}
    public void Drive(){}
    public void StartEngine(){}
    public void StopEngine(){}
}

修正後結果

public void Main()
{
    IDriver o = new Driver();
    o.StartEngine();
    o.Drive();
    o.StopEngine();
}
public interface IDriver
{
    void StartEngine();
    void Drive();
    void StopEngine();
}
public class Driver:IDriver
{
    public void Drive(){}
    public void StartEngine(){}
    public void StopEngine(){}
}
public interface IMachanic
{
    void ChangeEngine();
}
public Machanic:IMachanic
{
    public void ChangeEngine()
    {
        throw new NotImplementedException();
    }
}

相依反轉原則DIP

  • A.High-level modules should not depend on low-level modules.Both should depend on abstractions.
  • B.Abstractions should not depend on details.Details should depend on abstractions.
  • A.高階模組不應該依賴於低階模組,兩者都應相依於抽象
    • 高階模組=>Caller(呼叫端)
    • 低階模組=>Callee(被呼叫端)
  • B.抽象不應該相依於細節,而細節則應該相依於抽象

關於DIP的基本精神

  • 所有類別都要相依於抽象,而不是具體實作
    • 可透過DI Container達到目的
  • 為了要達到類別間鬆散耦合的目的
    • 開發過程中,所有類別之間耦合關係一律透過抽象介面

常見的設計問題

  • 類別與類別之間緊密耦合,改A壞B的狀況層出不窮
public class Client
{
    Service _Service;
    public void Client()
    {
        Service fooObj= new Service();
    }
}
public class Service();

什麼是相依反轉?

public class Client
{
    IService _Service;
    public void Client(IService service)
    {
        _Service = service;
    }
}
public interface IService{}
public class MyService:IService{}
public class YourService:IService{}
public class TheirService:IService{}

關於DIP的實作方式

  • 型別全部都相依於抽象,而不是具體實作
  • 經過套用DIP之後,原來有相依於類別的程式碼
    • 都改成相依於抽象型別
    • 緊密耦合關係變成鬆散偶合關係
    • 可以依據需求,隨時抽換具體實作類別

關於DIP的使用時機

  • 像要降低耦合的時候
  • 希望類別都相依於抽象,讓團隊可以更有效率的開發系統
  • 想要可以替換具體實作,讓系統變得更有彈性
    • 符合DIP通常也意味者符合OCP與LSP原則
    • 只要再多考量SRP與ISP就很棒了!
  • 想要導入TDD(測試驅動開發)單元測試的時候

DIP討論事項

  • 你在工作中是否有遇過類似的設計方式?(相依注入)
  • 如果一個類別非常穩定,也沒有變更需求,需要套用DIP嗎?
  • 大量套用DIP有缺點嗎?

練習情境

請試者找出底下程式碼不符合 相依反轉原則地方

public class SecurityService
{
    public bool LoginUser(string userName,string password)
    {
        LoginService service = new LoginService();
        return service.ValidateUser(userName,password);
    }
}
public class LoginService
{
    public bool ValidateUser(string userName,string password)
    {
        throw new NotImplementedException();
    }
}

修正相依於抽象,使用建構式傳入具體實作物件

public class SecurityService
{
    private readoly ILoginService _LoginService;
    public SecurityService(ILoginService loginService)
    {
        this._LoginService =loginService;
    }
        public bool LoginUser(string userName,string password)
    {
        return _LoginService.ValidateUser(userName,password);
    }
}
public void Main()
{
    new SecurityService(new LoginService());
}

https://moushih.com/

You may also like

Leave a Comment