C# Object Oriented Programming SOLID Principles
2023-01-04
筆記關於 Object Oriented Programming SOLID Principles 的內涵與使用時機。
說明
學習 Steve Smith 的 SOLID Principles for C# Developers 課後筆記。
- Single Responsibility Principle
- Open / Closed Principle
- Liskov Substitution Principle
- Interface Segregation Principle
- Dependency Inversion Principle
Single Responsibility
每一個模組都應該只有唯一的理由需要被改變
雖然你可以擁有一把萬用的瑞士刀,但不代表瑞士刀是唯一的選項。
模組的定義可以是物件導向中的類別 (Class) 或者是方法 (Method),不要為模組賦予過大的責任與工作,如果有不同的理由卻需要對相同的類別或者是方法進行修正時,就應該重新設計相關的程式碼,劃分更小的模組單位。
而為什麼模組不要有過大的責任與工作?對開發工作而言,閱讀程式碼的比例遠勝撰寫程式碼,減少模組的工作量可以降低在變更時需要反覆閱讀的程式碼量,從而提升開發與維護的效率。此外如果模組有過大的責任與工作量,也容易發生改 A 壞 B、牽一髮動全身的問題。
如何應用
原本龐大模組中的各項功能,可能包含訊息處理 (Logging)、資訊儲存 (Persistence)、資料驗證 (Validation) 以及各式的商業邏輯,這些功能可能都是以指令或是程式碼的方式存在。而為達到 Single Responsibility 可以將上述的功能拆分在不同的類別或者是方法,原本的類別則扮演組合與控制器的功能,調度相關的模組達成既有的需求功能。
// Logging
Console.WriteLine("Info");
// Persistence
File.ReadAllText("data.json");
// Validation
if (String.IsNullOrEmpty(userInput))
// Business Login
var rate = 0.5;
if (DateTime.Today.Year >= 2022){
rate += 0.1;
}
public class ConsoleLogger
{
public void Log(string message)
{
Console.WrteLine(message);
}
}
public class DataStorage
{
public string ReadFromSource()
{
return File.ReadAllText("data.json");
}
}
public class RateCalculator()
{
public double Rate {get;} = 0.5;
public double GetRate(){
if (DateTime.Today.Year >= 2022){
Rate += 0.1;
}
return Rate;
}
}
Open Closed Principle
類別對擴充開放、但對修改封閉
如果只是要裝新車燈,不需要動手改造整個引擎。
如何能夠做到既保持對擴充開放,但又對修改進行封閉?當有需求的時候,擴充不發生在原始的類別身上,而是另於其他的類別來達成,因此為對修改封閉;而之所以能夠在其他的類別達成,就是要做到對擴充開放,不會因為有新的擴充與類別實踐而破壞既有的功能。更精細的說法是需求所帶來的新的功能 (Behavior) 需求,可以藉由擴充的方式加入,但不需要對原始碼做異動。
為什麼不要對原始碼做異動?避免加入 Bug 以及新的條件判斷 (Conditions) 到原本的原始碼當中,導致未來的維護更加複雜。
如何應用
在實踐 Open Closed Principle 上,可以透過物件導向的參數 (Parameters)、繼承 (Inheritance) 以及 Composition & Injection 來達成。
而在設計上,採取建立新類別 (New Clasess) 的方式,除達到 Open Closed 的需求外,也符合 Single Responsibility,讓類別專注一件事情的設計方式。
Liskov Substitution Principle
子型別必須可以替換取代父型別
看起來像鴨子、叫起來也像鴨子,但它需要電池,必然搞錯了抽象。
物件導向存在 IS-A 以及 HAS-A 的關係,而 Liskov Substitution 指出 IS-A 關係的不足,相對於較為寬鬆的 IS-A Polymorphism 多形,必須要進一步限制其可以達到子型別替代父型別的 IS-Substitution。
這是為了解決例如物件轉型、Null 以及 NotImplementedException 等造成程式碼功能非預期的情況。
依循「Tell, Don't Ask」原則,不需要主動的識別物件的型別,物件應該會根據 context 進行對應的處理與邏輯。
Nulls Break Polymorphism | Ardalis.com
深入淺出 Liskov 替換原則 Liskov Substitution Principle
如何應用
在類別設計上,如果要實作 Interface 或者是繼承類別,必須要實作來源所有的屬性與方法,避免發生 NotImplementedException 以及避免主動檢查物件轉型的問題。
而如果來源需要實作的屬性與方法太多,就可以透過下一個原則 (Interface Segregation Principle) 來處理。
Interface Segregation Principle
避免依賴不需要的方法
魚缸不需要兼具筆筒的功能,對於使用者而言會很困擾
Interface 包含物件導向設計的介面 (Interface) 以及類別提供外部存取的屬性與方法 (Public Access)。而在設計 Interface 上,如果 Interface 過於龐大,包含眾多的屬性與方法,則實作該 Interface 的類別可能會出現許多非必要的方法或者保持為 NotImplementedException。因此應該設計「適當」的大小的 Interface,而適當是取決於內聚 (Cohesion) 的程度,但從 Interface 所包含的方法數觀察,可以是一個直觀的數字。
如何應用
將 Interface 的「大小」減少,不要龐大的 Interface 設計,而是要設計出高內聚 (Cohesion) 的輕量 Interface。
而在重構程式碼的過程,可以使用 Interface 的繼承關係來從大化小,並且使用 Aapter Pattern 來處理。
而在應用上,Interface Segregation 除了基於相容既有類別使用繼承關係外,也可以創造新的 Interface 來限制類別可以被存取的方法與屬性。
public interface Document {
void save();
void load();
void print();
void spellCheck();
}
public interface Document {
void save();
void load();
}
public interface PrintableDocument {
void print();
}
public interface SpellCheckableDocument {
void spellCheck();
}
Dependency Inversion Principle
高階模組不應該應賴低階模組,兩者都應該依賴抽象
你不需要把電器焊在電線上才能使用它
藉由抽象,我們可以解放物件的依賴關係達到解耦,為維護與擴充帶來更大的彈性與便利。
如何應用
明確 (Explicit) 的處理依賴,視為食譜的材料一般的完整呈現依賴。
使用依賴注入,從 Client 來產生依賴,而不要在模組之間產生依賴,依賴注入包含 Constructors Arguments, Properties 以及 Method Arguments 等方式,最常使用的是 Constructors Arguments。
使用 Constructors Arguments 來進行依賴注入的好處在於確保類別不會處於未初始化的狀態 (因為執行建構子),此外可以結合 Inversion Of Control (依賴反轉) Container 來進行依賴注入的控制管理。