--- tags: 設計模式 --- # 設計模式前置知識 ## 抽象化 >抽象化(英語:Abstraction)是指以縮減一個概念或是一個現象的資訊含量來將其廣義化(Generalization)的過程,主要是為了只保存和一特定目的有關的資訊。例如,將一個皮製的足球抽象化成一個球,只保留一般球的屬性和行為等資訊。相似地,亦可以將快樂抽象化成一種情緒,以減少其在情緒中所含的資訊量。 > Abstraction describe **what** > e.g. > - Send a Message > - Store a Customer record > > Details specify **how** > e.g. > - Send an SMTP email over port 25 > - Serialize Customer to JSON and store in a text file 由 Wiki 的定義 , 我們可以知道抽象化是 **針對某個事物或是概念 , 僅使用你對其感興趣或是覺得重要的資訊特徵(Feature)來描述** 這樣做的好處是 當**我們將現實事物或概念抽象化為類別後 , 其在呈現上會比原本的樣子更簡單 , 這可幫助我們在處理問題時 , 不需要處理過多的雜訊** ```C# // 僅使用兩個 Feature 來描述 Student public class Student { public string Name { get; set; } public int Age { get; set; } // 若我對學生體重不感興趣,那我就不會將其加入 Student 中 // public double Weight{ get; set; } } // 對於 Area 的計算 , 僅列出我們重視或是需要的形狀計算即可. "不需要盡數列出" public class AreaHelper { public static double GetCircleArea(int r) => r * r * Math.PI; public static double GetRectangleArea(int h, int w) => h * w; } ``` PS : 建立一個類別很簡單 , 但如何建好一個類別 , 卻很困難XD. ##### 參考資源 [抽象化WIKI](https://zh.wikipedia.org/wiki/%E6%8A%BD%E8%B1%A1%E5%8C%96) ## 高內聚 & 低耦合 高內聚和低耦合是我們用來評估程式是否寫得好的一個重要指標 , 也是目標 !!! ### 高內聚 > - 在計算機科學中,內聚性是指機能相關的程式組合成一模組的程度。應用在物件導向程式設計中,若服務特定型別的方法在許多方面都很類似,則此型別即有高內聚性。在一個高內聚性的系統中,代碼可讀性及復用的可能性都會提高,程式雖然複雜,但可被管理。 > - Cohesive means class elemeents that bolong together > - Cohesive describes how closely related elements of a class or module are to one another - 任何類別在建立時 , 必定有其建立的目標或原因 , 若此類別成員都與此目標有關 , 我就會說這個類別具有高內聚. ```C# public class MyRect { public int Height { get; set; } public int Width { get; set; } // 可以發現此方法的目標是計算外來物件的 Area , 這與 MyRect 物件沒有太大的關聯. // 除非這個方法是存在於一個專門計算 Area 的類別. // public static double GetRectArea(MyRect rect) => rect.Height * rect.Width; // 也許寫成這樣會比較好 .... ? // public double GetRectArea() => Height * Width; } ``` - 盡量讓類別內的成員都具有相同的目標, 若類別內有不同的目標, 可將大類別重構成複數個小類別 ```C# // MethodA 沒使用到 _b // 而 MethodB 只使用到 _b // 故 AandB 可以考慮拆成兩個內聚力更高的 class public class AandB { private int _a; private int _a1; private int _b; public int MethodA() => _a + _a1; public int MethodB() => _b; } ``` ```C# // A and B 的內聚力 高於合再一起時的 AandB public class A { private int _a; private int _a1; public int MethodA() => _a + _a1; } public class B { private int _b; public int MethodB() => _b; } ``` ### 低耦合 > - 耦合性(英語:Coupling,Dependency)或稱耦合力或耦合度,是一種軟體度量,是指一程式中,模組及模組之間資訊或參數依賴的程度。 > - Coupling meas Binds two (or more) details together in a way that's diffucult to change #### 低耦合定義 > - Offers a modular way to choose which details are involved in a particular operation > - Approaches that can be used to support having different details of the application interact with one another in a modular fashion - 應該盡可能讓一個 Module 的改變不會影響到另外一個 Module. ```C# // 舉一些可能會影響無法達到低耦合目標的行為. // 假設 OtherClass 在好多地方被使用 , 以下是其中之一. public class MyClass { // 假設需求改變 , 然後我們就直接修改 OtherClass 的實作 , 是否會影響到 MyMethod 的運作... ? public MyMethod(OtherClass otherClass) { // 當 OtherClass DoSomeThing() 實作修改或簽章移除的時候 , 可能會影響到 MyMethod var result = otherClass.DoSomeThing(); // 當 OtherClass Prop 實作修改或簽章移除的時候 , 可能會影響到 MyMethod var prop = otherClass.Prop; // New 是造成強偶合的行為. var obj = new OtherClass() } } ``` ##### 直接耦合 ```C# public class B{} public class A{ // A 直接耦合 B B Bp { get; } = new B(); } ``` ##### 抽象耦合 ```C# public interface IB{} public class B : IB {} public class B2 : IB {} public class A{ // A 耦合 IB , 再耦合 IB 的實作 public A(IB b){} } ``` PS : 通常我們會讓高位模組不知道其真正使用的低位模組的型別 , 所以我們會盡可能使用抽象耦合 , 而非直接耦合 ##### 參考資源 [内聚性(電腦科學)](https://zh.wikipedia.org/wiki/%E5%85%A7%E8%81%9A%E6%80%A7_(%E8%A8%88%E7%AE%97%E6%A9%9F%E7%A7%91%E5%AD%B8)) [耦合性](https://zh.wikipedia.org/wiki/%E8%80%A6%E5%90%88%E6%80%A7_(%E8%A8%88%E7%AE%97%E6%A9%9F%E7%A7%91%E5%AD%B8)) ## 繼承 & 封裝 & 多型 OOP 三大特性 , 若能善加運用這三個特性 , 可以幫助我們寫出更好的程式 ### 繼承 > 繼承(英語:inheritance)。如果一個類別B「繼承自」另一個類別A,就把這個B稱為「A的子類」,而把A稱為「B的父類別別」也可以稱「A是B的超類」。**繼承可以使得子類具有父類別別的各種屬性和方法,而不需要再次編寫相同的代碼。在令子類別繼承父類別別的同時,可以重新定義某些屬性,並重寫某些方法,即覆蓋父類別別的原有屬性和方法,使其獲得與父類別別不同的功能**。另外,為子類追加新的屬性和方法也是常見的做法。 #### 使用繼承的原因 - 為了 Code Reuse <--- 舊時代的認知 - 為了能使用多型 (Polymorphism) , 以便享受多型帶來的好處 <--- 現在的認知 ### 封裝 > - 封裝(英語:Encapsulation)是指,一種將抽象性函式介面的實作細節部份包裝、隱藏起來的方法。同時,它也是一種防止外界呼叫端,去存取物件內部實作細節的手段,這個手段是由程式語言本身來提供的。封裝被視為是物件導向的四項原則之一。 > - 封裝可以隱藏成員變數以及成員函式,物件的內部實現通常被隱藏,並用定義代替 - 對於資料的隱藏 - 對於實作的隱藏 - 對於真實型別的隱藏 ```C# public interface IMyInterface { string GetName(); // 隱藏實作 - 外界不需要知道實際的實作方式 } public class MyClass : IMyInterface { // 隱藏資料 - 外界不需要知道其內部使用哪些成員 private string _importantVariable = "private data 1"; public string GetName() => _importantVariable } public class MyClass2 : IMyInterface { // 隱藏資料 - 外界不需要知道其內部使用哪些成員 private string _importantVariable = "private data 2"; public string GetName() => _importantVariable } public class Caller { public void Exec(IMyInterface interface) // 隱藏真實型別 - Caller 不需要知道真實型別 { // do some thing } } ``` ### 多型 > 多型(英語:polymorphism)**指為不同資料類型的實體提供統一的介面,或使用一個單一的符號來表示多個不同的類型** 多型的最常見主要類別有: > - 特設多型:為個體的特定類型的任意集合定義一個共同介面。 > - 參數多型:指定一個或多個類型不靠名字而是靠可以標識任何類型的抽象符號。 > - 子類型(也叫做子類型多型或包含多型):一個名字指稱很多不同的類別的實例,這些類有某個共同的超類 > 多型 : 一個訊息的含意並非由發出訊息的人決定, 而是由接受訊息的人來決定 - 特設多型 (多載) ```C# // 統一介面為 GetName ; 當使用者使用時 , 才會知道使用到哪一個多載方法 public class MyClass { public string GetName() => "Name"; public string GetName(string name) => name; } ``` - 參數多型 (泛型) ```C# // 統一介面為 T ; 只有當使用者使用時 , 才會知道 T 為何 public class MyClass<T> { } ``` - 子類型 (繼承式多型) --- 使用變數型別為父類別 , 但真實型別為哪一個子類別則不知 ```C# // 統一介面為 IMyInterface ; 只有當使用者使用時 , 才會知道真實型別為何 public interface IMyInterface { string GetName(); } public class MyClass : IMyInterface { private string _importantVariable = "private data 1"; public string GetName() => _importantVariable } public class MyClass2 : IMyInterface { private string _importantVariable = "private data 2"; public string GetName() => _importantVariable } public class Caller { public void Exec(IMyInterface interface) // 不知道是哪一個子類別被傳入 { // do some thing } } ``` ##### 參考資源 [繼承 (電腦科學)](https://zh.wikipedia.org/wiki/%E7%BB%A7%E6%89%BF_(%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6)) [封裝 (物件導向程式設計)](https://zh.wikipedia.org/wiki/%E5%B0%81%E8%A3%9D_(%E7%89%A9%E4%BB%B6%E5%B0%8E%E5%90%91%E7%A8%8B%E5%BC%8F%E8%A8%AD%E8%A8%88)) [多型 (電腦科學)](https://zh.wikipedia.org/wiki/%E5%A4%9A%E6%80%81_(%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%A7%91%E5%AD%A6)) ## SOLID 設計原則 遵守 SOLID 設計原則可以幫我們更好地達成高內聚以及低耦合的目標. ### 單一職責原則 Single Responsibility Principle (SRP) #### 關注點分離(Separation of Concerns) > - Programs should be separated into distinct sections, each addressing a separate concern, or set of information that affects the program. > - 關注點分離(Separation of concerns,SOC)是對只與「特定概念、目標」(關注點)相關聯的軟件組成部分進行「標識、封裝和操縱」的能力,即標識、封裝和操縱關注點的能力。是處理複雜性的一個原則。由於關注點混雜在一起會導致複雜性大大增加,所以能夠把不同的關注點分離開來,分別處理就是處理複雜性的一個原則,一種方法。 > 關注點分離是面向對象的程序設計的核心概念。分離關注點使得解決特定領域問題的代碼從業務邏輯中獨立出來,業務邏輯的代碼中不再含有針對特定領域問題代碼的調用(將針對特定領域問題代碼抽象化成較少的程式碼,例如將代碼封裝成function或是class),業務邏輯同特定領域問題的關係通過側面來封裝、維護,這樣原本分散在整個應用程式中的變動就可以很好的管理起來。 - 舉個例子 , High Level Module 不應該知道實作細節 , 而是應該將實作細節封裝到 Low Level Module , High Level Module 只需要負責在如何使用 Low Level Module 就好. - High Level Module 關注如何使用 Low Level Module - Low Level Module 關注如何完成實作 #### SRP 定義 > Each class in your system should have only one **responsibility** > Each **module** should have one and **only one reason to change** > ps : A module might refer to a class or interface or even a single function. - 單一職責原則不是指一個模組只能做一件事情 , 而是指一個模組應該只有一個職責 , 意即只有一個原因會導致你想去改變. ```C# // clean code bad sample // Employee 負責計算員工的工作時數以及月薪 // 1.如果會計部想改變薪水計算方式 , 要改 CalculateMonthlySalary() // 2.如果人資想改變加班時數計算方式 , 要改 ProduceMonthlyHoursReport() public class Employee { // 當人資部請你修改時數計算方式時 , 則你去修改了 ProduceMonthlyHoursReport 方法 // 但你沒發現此修改會導致 CalculateMonthlySalary() 的計算結果因此不正確. public int CalculateMonthlySalary() { // ... var hp = ProduceMonthlyHoursReport(); // ... } public HoursReport ProduceMonthlyHoursReport() { //... } } ``` ```C# // clean code good sample // 將職責拆出去. 讓每個類別他們各自的成員 , 都與其類別的創造目的有關. // 提升內聚 , 降低耦合 // 讓類別比較小以及簡單 , 也比較好維護 ~ class Employee { private string _id; public string getId()=> _id; } class PaymentService { public int calculateMonthlySalary(Employee employee) { //... } } class WorkHoursService { public HoursReport produceMonthlyHoursReport(Employee employee) { //... } } ``` - 實務上 class 很可能無法完美遵守單一職責 , 因為每個人對於是否單一職責可能都有不同的見解.但不論如何 , 都應該盡可能保持較少的職責 ```C# // 1. Bank 負責錢的相關業務. 一個職責(?) // 2. 若認為其是兩個職責(負責存錢/負責提錢) , 則有可能發生 // "想改變存錢的邏輯" or "想改變提錢的邏輯" // 此時須將 SaveMoney 及 WithdrawMoney , 各自獨立成一個 Service Class // (各自負責存錢/提錢的業務) // 再透過組合讓 Bank 類別使用這兩個 Service class 以完成工作 public class Bank { public void SaveMoney() { // do some thing } public void WithdrawMoney() { // do some thing } } ``` - 方法以及介面必須要嚴格遵守單一職責 ```C# // bad code sample public interface MyInterFace // <-- 這介面超難用 { // 100 個以上的成員 } public void MyMethod() // 做了超多事情 { // 3000 行以上的程式碼 } ``` PS: 實務上 , 很可能無法一開始就實現這個原則 , 即使一開始是 , 也有可能在之後的開發 , 隨著不斷疊加新的功能而變得沒有滿足這個原則的要求. 當你發現一個類別負責太多功能(過於龐大) , 導致難以維護時 , 請適時的進行類別的重構 , 將類別中實作相關功能的部分抽取出來並封裝成一個新的類別 , 再透過組合的方式將新增的類別加入原類別之中 , 以此慢慢再度達成單一職責的要求. ##### 參考資源 [深入淺出單一職責原則 Single Responsibility Principle](https://www.jyt0532.com/2020/03/18/srp/) [單一功能原則](https://zh.wikipedia.org/wiki/%E5%8D%95%E4%B8%80%E5%8A%9F%E8%83%BD%E5%8E%9F%E5%88%99) > 一個類或者模塊應該有且只有一個改變的原因。 ### 開放封閉原則 Open-Closed Principle (OCP) ##### 定義 : 對擴展開放 , 對修改封閉 > Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification. > It should be possible to change the behavior of a method without editing its source code - 對修改封閉 - 所有公開出去讓人能看到的介面/接口/簽章, 應該**盡可能不去修改**. 例如: 改名或是新增移除. e.g. 公開方法 or 公開屬性等等 - **對修改封閉指的是應該有一個盡可能不會改變的抽象** , 並且透過此抽象可以產生彈性的實作 e.g. 透過繼承式多型來增加功能 - **設計類別時**應思考問題的 Hot Spot 為何(需要留彈性的地方)並且將之**固定**, 以便之後若有新的同性質需求時 , 可以透過繼承式多型應對. - 對修改封閉意味著, 我們高機率不需要去改變來源程式碼. - 只有一種方式能夠改變對擴展封閉的程式碼的行為, 那就是改變它的程式碼, 這種情況代表沒有滿足對修改封閉的要求 - Bug Fixes 應該視為 OCP 原則的例外 - 對擴展開放 - **對型別拓展** 以對應新的需求 - 新建立一個能處理新需求的類別並且讓其繼承某個抽象型別. - 對擴展開放意味著, 程式是容易新加入相同固定概念的新行為 - 對擴展封閉意味著, 程式的行為已經固定, 除非我們修改來源程式碼. - 常見 OCP 的實作方式 - 參數 - 傳入參數給方法 ```C# public class DoOneThing { // 行為將依據傳入參數而改變 <-- 印的字串會不同 public void Do (string name) => Console.Write(name) } ``` - 繼承 (為了多型) - virtual & override 方法 ```C# public class DoOneThing { public virtual void Do () => Console.Write("Hi C#") } public class DoTwoThing : DoOneThing { public override Do () => Console.Write(" Hello World ") } ``` - 組合 & 依賴注入 ```C# public class DoOneThing { // 產生 Message 的責任被移動到 IMessageService, DoOneThing // 僅需要注入 IMessageService 後使用 private readonly IMessageService _messageService; public DoOneThing(IMessageService messageService) => _messageService = messageService; public virtual void Do () => Console.Write($"{_messageService.GetNessage()}") } ``` - Benefits of OCP - 比較不可能因為修改程式碼而去破壞相依程式碼的正確性 - 比較不可能製造新的 Bug 在我們沒修改的程式碼 - OCP 希望新的需求的行為應該透過實作新的程式碼達成. 因為不會去修改到舊的程式碼 ,所以舊的程式碼理論上不應該會產生新的 Bug - 遵守擴展開放定義的程式碼 , 通常會具有比較少地條件判斷(比較好閱讀) , 且比較容易測試. #### OCP Summary - 盡可能在新類別實作新功能 - 理由 - 可針對新問題/需求去設計新類別 - 新類別沒有舊有程式碼的包袱 (尚無任何程式碼依賴他) - 可增加新行為, 但不需要修改舊有程式碼 - 能較為輕鬆, 實作出遵守 SRP 的類別 - 能較為輕鬆, 實作出容易測試的類別. - 過多的抽象反而可能會增加程式碼複雜度 (太多層) - Abstract and Concreteness 需要取得平衡 - 如何能預測未來變化, 並套用 OCP ? - 不需要 100 % 預測未來的變化, 實作時, 可以先依照現有資訊完成一個版本. - 可先使用簡單的方式實作 - 依據現有資訊預測變化, 預留抽換的縫隙 - 觀察程式碼修改狀況, 若某段程式碼經常被修改( e.g. 超過三次 ), 則應該考慮遵守 OCP 原則, 重構其為遵守 OCP 的程式碼. - 按照過去修改經驗可知, 未來高機率會再修改, 因此重構其為遵守 OCP 的程式碼, 在未來修改時, 可享有 OCP 帶來的好處. - 試圖辨識 **改變** 是否能解決真實需求 - 不要因為猜測的需求 (無法確認是否需要), 進而修改程式 - 為了那些你已經辨認出的需求(會持續出現的新需求) , 而修改程式為 **對擴展開放** , - 避免每一次都為了達成新需求 , 而修改 Source Code ##### OCP Example ```C# // code sample public abstract class 加密演算法 //<-- 盡可能不會改變的抽象 (也可以用 Interface) { public virtual void 加密() { // 某種簡單的加密 } } // 新的需求是用 MD5加密檢算法加密 public class MD5加密演算法 : 加密演算法 //<-- 對型別拓展 , 以對應新的需求 { public override void 加密() { // MD5 加密 } } public class Customer { // Customer 不知道"加密演算法"的真實型別. 未來有新的需求 , Customer 也不需要改變任何程式碼 public DoSomeThing(加密演算法 algorithm) { // 使用 algorithm 去加密某個東西 } } ``` ##### 參考資源 [深入淺出開放封閉原則 Open-Closed Principle](https://www.jyt0532.com/2020/03/19/ocp/) ### 里式替換原則 Liskov’s Substitution Principle (LSP) ##### 定義 : 子類別必定能夠替換父類別 > If S is a subtype of T, then objects of type T may be replaced with objects of type S without altering any of the desirable properties of the program (correctness, task performed, etc.) > Subtypes must be substitutable for their base types - 里式替換原則要求我們必須**正確的使用繼承** : 我們在使用繼承之前 , 必須先判斷子類別是否為父類別的 SubType.( 軟體使用到的任何父類別的地方 , 都可以用其子類別去替換). 若其不是 SubType , 則不應該使用繼承 , 因為**繼承會造成強相依**. 例如 : 鴕鳥繼承鳥類. 鳥類會飛 , 但鴕鳥不會飛 , 故使用鳥類的地方 , 不能使用鴕鳥去替換. 所以我們只能說鴕鳥是鳥類的 SubClass , 但不能說是 SubType. - 里式替換原則要求我們要達成 - **子類別必須擁有所有父類別的成員** : 父類別所具有的能力(公開方法 or 公開屬性) , 子類別可以透過繼承來擁有(雖然實作細節可能因為 override不同. 但即使子類別 override 也不應該影響原本父類別的使用). 此可以讓子類別具備父類別的特性 , 以保證子類別可以取代父類別的使用 - **子類別不應該隱藏父類別的成員** - 子類別可以新增屬於自己的成員 - 子類別可以 override 父類別的成員 - 當子類別多載 (overload) 父類別方法時,方法的參數要比父類別更寬鬆 ```C# public class BaseClass { public void Exec(string value) => Console.WriteLine($"{nameof(BaseClass)} {nameof(Exec)} {value}"); } public class DerivedClass : BaseClass { public void Exec(object value) { // 需要加入這些程式碼以避免變數型別為基底時,行為會不一致 <-- 違反 LSP // if (value is string s) // { // base.Exec(s); // return; // } Console.WriteLine($"{nameof(DerivedClass)} {nameof(Exec)} {value}"); } } ``` - 當子類別覆寫 (override) 父類別抽象方法的時候,方法的回傳值要比父類別更嚴格 ```C# // code sample public class A{ public virtual void CanOverrideMethod() { // do some thing } public void Method() { // do some thing } // 若 A 擁有一個 B 不具備的能力. //public virtual void NotCanOverrideMethod(){ // do some thing} } public class B : A { public override void CanOverrideMethod() { // do some thing } // 若 B 類別不具備 NotCanOverrideMethod 的能力 , 則無法達成里式替換原則的要求 //public override void NotCanOverrideMethod()=> throw new NotImplementedException(); // 隱藏父類別的成員 Method -> 這會導致無法遵守里式替換原則 // public new void Method(); } public class User { public void Use() { //不論 obj 實際型別為何, User 都應該可以使用 CanOverrideMethod 以及 Method A obj = new A(); //<-- 可以被替換成 A obj = new B() obj.CanOverrideMethod(); obj.Method(); } } ``` - 判斷程式碼是否有可能違反 LSP - 是否有 is or as 關鍵字 ```C# public void Method (SomeObj obj){ if( obj is Aobj ) // Do Some Thing else if ( obj is Bobj) // Do Another Thing // .... } ``` - 可考慮使用多型避免 - 可考慮使用 Helper Method (將判斷型別的的動作抽出到 static Helper Method) - 是否有 Null Checking ```C# public void Method (IEnumerable<SomeObj> objs) { foreach(var obj in objs) { if(obj is null) { // do some thing; break; } // Do Normal Thing; } } ``` - Null Object Pattern. - C# feature - Null Conditional Operators ( ?. ) - Null Coalescing Operators ( ?? or ??= ) - Nullable Reference Type ( Type? type ) - Guard clauses ( Nuget 套件 ) - 是否有 NotImplementedException ```C# public void Method (){ throw new NotImplementedException(); } ``` - 里式替換原則是繼承式多型可以正常運作的原因. - 里式替換原則提供開放封閉原則一個實作的機制.(繼承式多型) - LSP 頃向製造 容易 Follow OCP 原則的程式碼 - 當我們達成里式替換原則的要求 , 代表客戶端並不需要知道真實使用的型別. ```C# //不應該在客戶端看到強制轉型成某個子類別的寫法 public class User { public DoA(A obj) { (obj as B)?.DoMethod() } } ``` ##### 參考資源 [深入淺出 Liskov 替換原則 Liskov Substitution Principle](https://www.jyt0532.com/2020/03/22/lsp/) ### 倚賴倒置原則 The Dependency Inversion Principle (DIP) #### 倚賴倒置原則定義 > 1. High-level modules should not depend on low-level modules. Both should depend on abstractions. > 2. Abstractions should not depend on details. Details should depend on abstractions. > - High-level modules > - More Abstract > - Business Rules > - Process-Oriented > - Further from input/output (I/O) > - Low-level modules > - Closer to I/O > - Plumbing code > - Interacts with specific external systems and hardware - 高位模組不應該依賴於低位模組 , 兩者都應該依賴於抽象 - 應該在高低位模組之間再加一層抽象 ```mermaid graph TD; 高位模組[高位模組] 低位模組[低位模組] 抽象[抽象 : 介面或是抽象類別] 高位模組--依賴--> 抽象 低位模組--繼承--> 抽象 ``` - ```C# public interface IProgrammable // <-- 抽象 { void Program(); } public class Computer : IProgrammable // <-- 低位模組 { public void Program() => throw new NotImplementedException(); } public class Programmer // <-- 高位模組 { // 高位模組 Programmer 依賴於抽象 IProgrammable // 若是未來 Programmer 需要使用 NoteBook 工作 , // 也僅需要新增一個繼承 IProgrammable 的 NoteBook , 傳給 Programmer 即可. // 不需要修改任何 Programmer 的程式碼 private readonly IProgrammable _programmable; public Programmer(IProgrammable programmable) => _programmable = programmable; public void Coding() => _programmable.Program(); } ``` - 抽象不應該依賴於細節 , 細節應該取決於抽象 --> 先寫抽象 , 再實作 - 抽象不應該倚賴於細節 - 不應該依據實際實作的細節去思考抽象後的產物. 而是以其具有的特徵. <-- 很難orz 例如 : 想要使用 RSA 加密 , 則應該先思考的是加密應該如何抽象化(e.g. 其他人會如何與加密介面型態互動) 而非 RSA 加密可以如何實作. --> 先寫抽象(加密) , 再實作(RSA加密演算法). ```C# public Interface 加密 // <-- 先思考"加密"介面的特徵 { void 加密(); } public class RSA加密演算法 : 加密 // <-- 再實作 RSA 加密演算法 { public void 加密() { // RSA加密 } } // 若是之後需要增加 DES 加密 , 因為接口處都是使用 加密介面. // 僅需要新建 DES加密類別 , 並且傳給 DES加密類別給高位模組就好. // 高位模組的程式碼不需要有任何的更動. public class DES加密演算法 : 加密 // <-- 再實作 DES 加密演算法 { public void 加密() { // DES加密 } } // 若低位模組已經就位,開始使用(沒有實作抽象層) // 可在日後需求修改 , 需要重構時 , 慢慢補上. ``` - 細節應該取決於抽象 - 按照抽象層(介面或是抽象類別)去實作方法細節. ```C# public interface IService{ //<-- 先已有介面 public Do(); } public class ServiceImplement : IService{ //<-- 再依據介面去決定實作 public Do(){ // Do some thing } } ``` ##### 參考資源 [深入淺出依賴反向原則 Dependency Inversion Principle](https://www.jyt0532.com/2020/03/24/dip/) ### 介面隔離原則 Interface Segregation Principle (ISP) > No client should be forced to depend on methods it does not use. > Clients should not be forced to depend on methods they do not use. > PS: client is the code that is interacting with an instance of the interface. - 客戶端不應該被強迫依賴它不需要的方法 - 子類別實作介面, 不應該被強迫實作其不需要的方法. 否則使用子類別的高位模組 , 可能會得到不可預期的錯誤. --> 高位模組並不會也不需要去注意到子類別的實作方式. ```C# interface IA{ int Method() int Method2(); } class A : IA{ // A 只需要使用到 Method public int Method(){ // 自己的實作. } // A 不具有 IA 的能力 --> 違反 LSP 原則 public int Method2() => throw new NotImplementException(); } class 高位模組{ IA a = new A(); a.Method2(); // 因為 IA 規定此方法必須實作 , 所以預期會正常運作 // 但結果是不正常運作 --> 噴例外 } ``` - 類別間的倚賴應建立在最小的介面上 <-- 由上面的原則定義所延伸出的方向 - 遵守這項原則對於介面的重複使用會有幫助 --> 一個介面若是只被實作一次 , 則需要思考此介面是否有存在的必要.(單元測試的需求例外.) - 可以透過介面是否滿足單一職責原則來判斷設計好壞. <-- 介面不應該太肥大/具有多項職責 ```C# // bad code sample interface I智慧手機 { // <-- 這個介面很難被重複使用 void 拍照(); //<--拍照和上網這兩個動作 , 明顯與手機沒有關聯 , 應該各別抽介面使用. void 上網() ; } class 智慧手機 : I智慧手機{ //實作介面成員 } ``` ```C# // good code sample interface 照相 { // <-- 只有拍照的職責 void 拍照() } interface 上網 { // <-- 只有上網的職責 void 上網() } class 智慧型手機 : 照相,上網 { // 實作介面成員 } class 普通手機 : 照相 { // 實作介面成員 } class 爛手機 { // 實作介面成員 } ``` - 如何判斷是否可能違反 ISP - Large Interfaces - NotImplementedException - Code uses just a small subset of a larger interface #### Summary - 應該讓介面盡可能小以及高內聚 (遵守 SRP) - Large Interface result in more Dependenceies. - More Dependenceies result .. - More coupling - Code is more brittle because of the increased coupling - More harder to Unit Test because fake implementations need to require more work. - 遵守 ISP 能幫助 SRP 以及 LSP - 拆分大介面為數個小介面的方式 - 介面繼承 - 參考上面智慧手機的例子 - Facade/Adapter Design Pattern ##### 參考資源 [深入淺出介面分割原則 Interface Segregation Principle](https://www.jyt0532.com/2020/03/23/isp/) [菜雞與物件導向 (13): 介面隔離原則](https://igouist.github.io/post/2020/11/oo-13-interface-segregation-principle/) ### 最少知識原則 Law of Demeter (LOD) > - Each unit should have only limited knowledge about other units: only units "closely" related to the current unit. > - Each unit should only talk to its friends; don't talk to strangers. > - Only talk to your immediate friends. - 一個物件應該對其他物件有最少了解 反過來說 , 一個類別不應該公開那些不需要讓人知道的資訊. 請善加使用 private 修飾詞以封裝那些外界不需要知道的資訊 , 因為你永遠不知道外界如何使用你的公開成員. ```C# // 範例 // 需求 : Login 之前必須先檢查帳號以及密碼 ``` ```C# // bad code sample class LoginService { public bool CheckAccount(){// 實作} public bool CheckPassword(){// 實作} public void Login(){ // 實作} } class 高位模組 { public ExecLogin(){ // 高位模組必須知道登入系統前必須"先檢查帳號以及密碼". <-- 需要知道正確執行的細節 // 當 LoginService 的正確使用細節越複雜 , 代表對高位模組來說 , 其越不親切(不好用). // 有誤用的可能性存在 -> e.g.高位模組可以不檢查就直接呼叫 login.Login(). 違反需求 var login = new LoginService(); if(login.CheckAccount() && login.CheckPassword()){ login.Login(); } } } ``` ```C# // good code sample class LoginService { private bool CheckAccount(){// 實作} private bool CheckPassword(){// 實作} public void Login(){ // <--- 外界只看的到 Login // 檢查帳號和密碼 , 若通過才登入系統. } } class 高位模組 { public ExecLogin(){ // 封裝外界不需要知道的細節到 LoginService , // 高位模組只需要看簽章 , 知道呼叫 Login() 就可以登入這件事就好. var login = new LoginService(); login.Login(); } } ``` ##### 參考資源 [Law of Demeter](https://en.wikipedia.org/wiki/Law_of_Demeter) [迪米特法则](https://baike.baidu.com/item/%E8%BF%AA%E7%B1%B3%E7%89%B9%E6%B3%95%E5%88%99/2107000) [[心得整理] c# 物件導向程式 - 6.LKP 最少知識原則](https://dotblogs.com.tw/initials/2016/07/03/141433) ## 控制反轉 Inversion of Control ### [定義](https://en.wikipedia.org/wiki/Inversion_of_control) > In software engineering, **inversion of control (IoC) is a programming principle. IoC inverts the flow of control as compared to traditional control flow.** In IoC, custom-written portions of a computer program receive the flow of control from a generic framework. A software architecture with this design inverts control as compared to traditional procedural programming: in traditional programming, the custom code that expresses the purpose of the program calls into reusable libraries to take care of generic tasks, but with inversion of control, it is the framework that calls into the custom, or task-specific, code. > Inversion of control is used to increase modularity of the program and make it extensible,[1] and has applications in object-oriented programming and other programming paradigms. > The term is related to, but different from, the dependency inversion principle, which concerns itself with decoupling dependencies between high-level and low-level layers through shared abstractions. The general concept is also related to event-driven programming in that it is often implemented using IoC so that the custom code is commonly only concerned with the handling of events, whereas the event loop and dispatch of events/messages is handled by the framework or the runtime environment. > In traditional programming, **the flow of the business logic is determined by objects that are statically bound to one another. With inversion of control, the flow depends on the object graph that is built up during program execution.Such a dynamic flow is made possible by object interactions that are defined through abstractions.** - 傳統程式設計中, 取得商業邏輯流程所需的物件是 Statically Binding, 而遵守 IOC 原則的設計則會是 Dynamic Binding - Statically Binding - 在編譯時期就已經知道它的型別 ```C# // a 只能是 A 型態的物件. var a = new A(); ``` - Dynamic Binding - 程式執行時, 才能知道型別. ```C# // 回傳值的真實型態取決於參數 isA1 // 實務上參數通常都會是 Type 型態. IOC 透過 Type 物件決定要建立什麼樣的物件回傳. public IA CreateA(bool isA1) => isA1 ? new A1() : new A2(); ``` - IOC 是一種設計程式的程式設計原則 , 其**目標是反轉高位模組對於依賴低位模組的『控制流程 (Control Flow).** - IOC 只是一種設計程式的程式設計原則. 所以實現它的方式非常多 , 像是 Service Locator Pattern 或是 Factory Pattern 以及 Dependency injection 等等方式都可能實現它 , **只要這種實現方式可以讓高位模組不需要對低位模組作流程控制(e.g. 自己去 new 低位模組物件) , 就可以說 , 它滿足 IOC 的要求.** - IOC 要求將流程的控制(物件的建立)交給第三方負責 , 以反轉控制流程 (Control Flow). --> 需要甚麼 , 就跟第三方要 , 透過第三方(負責 new 物件,再回傳)得到所需物件. - **雙方都依賴某個型別(通常是抽象型別), 藉此降低雙方的耦合** - 目前常見的 IOC 的**實現方式是將低位模組物件的建立交給 IOC 容器 負責 , 再透過 DI 的方式將其傳遞給高位模組** - 不使用 IOC 的流程圖 (高位模組強相依於 A,B,C) ```mermaid graph TD; A[類別A] B[類別B] C[類別C] 高位模組[High-Level Module - 使用者] 高位模組 --高位模組自己 new A--> A 高位模組 --高位模組自己 new B--> B 高位模組 --高位模組自己 new C--> C ``` - 使用 IOC 流程圖 ```mermaid graph TD; A[類別A] B[類別B] C[類別C] IOC[IOC 容器] 高位模組[High-Level Module - 使用者] 高位模組 --跟 IOC 容器取得所需物件--> IOC IOC --IOC 負責產生物件 a--> A IOC --IOC 負責產生物件 b--> B IOC --IOC 負責產生物件 c--> C ``` ## 依賴注入 (Dependency Injection) ### [定義](https://en.wikipedia.org/wiki/Dependency_injection) > In software engineering, **dependency injection is a technique in which an object receives other objects that it depends on, called dependencies.** Typically, the receiving object is called a client and the passed-in ('injected') object is called a service. The code that passes the service to the client is called the injector. Instead of the client specifying which service it will use, the injector tells the client what service to use. The 'injection' refers to the passing of a dependency (a service) into the client that uses it. > The service is made part of the client's state.[1] Passing the service to the client, rather than allowing the client to build or find the service, is the fundamental requirement of the pattern. > The intent behind dependency injection is to achieve separation of concerns of construction and use of objects. This can increase readability and code reuse. > **Dependency injection is one form of the broader technique of inversion of control.** A client who wants to call some services should not have to know how to construct those services. Instead, the client delegates to external code (the injector). The client is not aware of the injector.[2] The injector passes the services, which might exist or be constructed by the injector itself, to the client. The client then uses the services. > This means the client does not need to know about the injector, how to construct the services, or even which services it is actually using. The client only needs to know the interfaces of the services, because these define how the client may use the services. This separates the responsibility of 'use' from the responsibility of 'construction'. #### 依賴注入是將高位模組所需的低位模組注入(提供)到高位模組中的一種技術 - 控制反轉是一種概念 , 希望可以反轉高位模組對於依賴低位模組的『控制流程 (Control Flow)』 - 依賴注入是透過某種方式把高位模組所需要的低位模組提供給高位模組. 所以可以把依賴注入想成是控制反轉的其中一種實作方式. 因為高位模組不需要自己產生低位模組 , 低位模組會透過某種方式被提供給高位模組. 而這就滿足了控制反轉所需要達成的目標. ### 常見的注入方式 - 建構子注入 ```C# // 先在 IOC 容器註冊 IMyApp & MyApp 以及 IWriter & ConsoleWriter public class MyApp : IMyApp // 高位模組 { private readonly IWriter _writer; // ConsoleWriter 會透過建構子被注入到 MyApp 中 public MyApp(IWriter writer) => _writer = writer; public void OutputString(string text) => _writer.Write($"you output {text}!"); } public interface IWriter // 抽象 { void Write(string s); } public class ConsoleWriter : IWriter // 低位模組 { public void Write(string s) => Console.WriteLine(s); } ``` - 透過建構子將高位模組(MyApp) 的低位模組(ConsoleWriter) 提供給高位模組(MyApp) - 屬性注入 - 方法注入 ## 簡單自製 IOC 容器範例 - IOC 容器通常會負責管理低位模組物件的生命週期 #### 自製 IOC 容器實作方式 ```C# public class MyContainer { private readonly Dictionary<Type, Type> _types = new Dictionary<Type, Type>(); public void Register<TKey, TImplementation>() where TImplementation : TKey => _types[typeof(TKey)] = typeof(TImplementation); public T Create<T>() => (T)Create(typeof(T)); public object Create(Type type) { //Find a default constructor using reflection var defaultConstructor = _types[type].GetConstructors()[0]; //Verify if the default constructor requires params var defaultParams = defaultConstructor.GetParameters(); //Instantiate all constructor parameters using recursion var parameters = defaultParams.Select(param => Create(param.ParameterType)).ToArray(); return defaultConstructor.Invoke(parameters); } } ``` #### 自製 IOC 容器使用範例 ```C# public interface IMyApp { void OutputString(string name); } public class MyApp : IMyApp { private readonly IWriter _writer; public MyApp(IWriter writer) => _writer = writer; public void OutputString(string text) => _writer.Write($"you output {text}!"); } public interface IWriter { void Write(string s); } public class ConsoleWriter : IWriter { public void Write(string s) => Console.WriteLine(s); } internal static class Program { private static void Main(string[] args) { // Create IOC Container var container = new MyContainer(); // Register Type container.Register<IWriter, ConsoleWriter>(); container.Register<IMyApp, MyApp>(); // Get MyApp Instance from IOC Container // 高位模組不需要自己 new MyApp 以及 ConsoleWriter 以及設定它們. 就可以使用它們 var myApp = container.Create<IMyApp> (); myApp.OutputString("QQQ"); //<--- you output QQQ! Console.ReadKey(); } } ``` ## 控制反轉 vs 依賴反轉 ### 控制反轉 不等於 依賴反轉 !!! - 依賴反轉 -> 倒轉的是物件之間的『依賴關係』 - 控制反轉 -> 倒轉的是物件之間的『控制流程』 ### 遵守控制反轉但不遵守依賴反轉 !? ```C# // 先在 IOC 容器註冊 Computer & Computer public class Computer { public void Program() => throw new NotImplementedException(); } public class Programmer { private readonly Computer _computer; // 透過建構子注入從 IOC 取得 Computer 物件 public Programmer(Computer computer) => _computer = computer; public void Coding() => _computer.Program(); } ``` 此程式違反了依賴倒置原則. 但 Programmer 仍然可以由 IOC 容器取得 Computer 物件. - IOC 解除了高位模組 (Programmer) 主動產生低位模組 (Computer) 的控制 , 卻解除不了高位模組對低位模組的依賴關係. 意即高位模組不負責正確生產低位模組 , 但高位模組仍然依賴於此低位模組. ### 遵守依賴反轉但不遵守控制反轉 !? ```C# // 這裡遵守依賴反轉 public interface IProgrammable { void Program(); } public class Computer : IProgrammable { public void Program() => throw new NotImplementedException(); } public class Programmer { private readonly IProgrammable _programmable; public Programmer(IProgrammable programmable) => _programmable = programmable; public void Coding() => _programmable.Program(); } ``` ```C# // 這裡不遵守控制反轉 <--- 出來混, 總是要還的. 總有一天要 New object 的 XD public class MainClass { private readonly Programmer _programmer; public MainClass() { // 自己 new Computer 物件 違反 IOC 原則 _programmer = new Programmer(new Computer); } } ``` - MainClass 自己 new 其依賴的物件 Programmer (違反 IOC 原則). - MainClass 需要多知道 Programmer 所依賴的 Computer 應該怎麼正確的初始化(new 後的設定). - 僅是將高位模組的依賴對象由具體改為抽象是不夠的. 因為欲使用高位模組(Programmer)時仍然需要自己 new 低位模組(Computer). 而這會導致使用此高位模組時 , 需要知道如何正確產生低位模組. > **依賴並沒有完全解除 !!!** ### 同時使用遵守依賴反轉以及控制反轉 ```C# // 先在 IOC 容器註冊 IProgrammable & Computer 以及 IProgrammer & Programmer public interface IProgrammable { void Program(); } public class Computer : IProgrammable { public void Program() => throw new NotImplementedException(); } public interface IProgrammer { void Coding(); } public class Programmer : IProgrammer { private readonly IProgrammable _programmable; // 透過建構子注入取得 Computer 物件 public Programmer(IProgrammable programmable) => _programmable = programmable; public void Coding() => _programmable.Program(); } ``` ```C# public class MainClass { private readonly IProgrammer _programmer; // 透過建構子注入取得 Programmer 物件 public MainClass(IProgrammer programmer) { _programmer = programmer; } } ``` - 在 IOC 容器中註冊 IProgrammer & Programmer 以及 IProgrammable & Computer , 然後 MainClass 可以直接由 IOC 容器中取得 Programmer 物件. - MainClass 不需要知道 Programmer 如何產生. 也不需要知道如何產生 Computer. - 未來若需求改變 , 需要將 Computer 抽換成 NoteBook 等等. 僅需要去 IOC 容器處修改註冊的程式碼即可. 並不需要修改所有產生 IProgrammable 相關的程式碼. - **高位模組依賴於抽象 , 而非低位模組. 但高位模組使用其依賴的抽象時. 不用也不需要知道是哪種實作該抽象的低位模組被產生. 高位模組僅須知道 IOC 容器會提供給它所需要的低位模組.** ![](https://github.com/s0920832252/C_Sharp/blob/master/Files/ioc-di-dependency1.png?raw=true) ### 總結 : 控制反轉 vs 依賴反轉 ```mermaid graph TD; A[依賴倒置原則] A1[開放封閉原則] A2[繼承式多型] B[控制反轉] C[IOC 容器] D[依賴注入] A--保證必定存在抽象可封閉-->A1 A1--封閉抽象,對擴展開放以享受多型帶來的好處-->A2 A-- 為了解決DIP實作上的困難而產生的解決思路 -->B B--負責產生管理物件的第三方-->C B--實現方式-->D ``` ![fFXjpEj.png](https://github.com/s0920832252/C_Sharp/blob/master/Files/DesignPattern/fFXjpEj.png?raw=true) - 依賴倒置原則告訴我們應該依賴於抽象 , 為了依賴於抽象 , 高位模組所需要的低位模組自然就不可能自己產生 , 而是從外界傳入. 但即使如此 , 仍然有某一層高位模組需要產生低位模組. 而這就導致了依賴(高位模組也不應該知道低位模組如何產生 , 因為應該依賴於抽象). 而 IOC 的設計原則可以解決這個問題. - IOC 希望我們將控制權交給第三方 (IOC 容器) 以避免高位模組相依於低位模組. 所以 IOC 容器會負責產生高位模組所需要的低位模組實例(需要有跟 IOC 容器註冊過) - 依賴注入是將高位模組所需要的低位模組傳遞給高位模組的一種設計模式. - IOC 可以讓你不需要自己去初始化相依的物件.(不用了解那些細節) - 可以把 IOC 容器想成是一個黑盒子 , 負責產生實例 ##### 參考資源 [你確定懂?徹底搞懂 控制反轉(IoC Inversion of Control )與依賴注入(DI Dependency Inversion Principle )](https://iter01.com/562085.html) [淺入淺出 Dependency Injection](https://medium.com/wenchin-rolls-around/%E6%B7%BA%E5%85%A5%E6%B7%BA%E5%87%BA-dependency-injection-ea672ba033ca) [控制反轉 (IoC) 與 依賴注入 (DI)](https://notfalse.net/3/ioc-di#-Dependency-Injection) [Writing a Minimal IoC Container in C#](https://www.encora.com/insights/writing-a-minimal-ioc-container-in-c) [控制反轉(Inversion of Control)與依賴注入(Dependency Inversion)](https://hackmd.io/@CityChen/H1TqIfczK) --- ###### Thank you! You can find me on - [GitHub](https://github.com/s0920832252) - [Facebook](https://www.facebook.com/fourtune.chen) 若有謬誤 , 煩請告知 , 新手發帖請多包涵 # :100: :muscle: :tada: :sheep: <iframe src="https://skilltree.my/c67b0d8a-9b69-47ce-a50d-c3fc60090493/promotion?w=250" width="250" style="border:none"></iframe>