SOLID Principle基礎入門

(Robert C. Martin (Uncle Bob))

什么是SOLID原則?

SOLID原則是面向對象編程(OOP)中編寫高質量代碼的指導方針。實際上,即使不使用SOLID原則,僅通過類、繼承、封裝和多態性,也可以讓程序正常運行。那么為什么會出現SOLID原則呢?

SOLID原則是為了提高代碼的可維護性 可擴展性 以及管理耦合度 而設計的一種指導方針。換句話說,SOLID原則是編寫高質量OOP代碼的指南。

Agile Software Development: Principles, Patterns, and Practices (2002)

SOLID原則的起源

SOLID原則最早出現在羅伯特·C·馬丁(Robert C. Martin,也被稱為Uncle Bob)于2002年出版的《敏捷軟件開發:原則、模式與實踐》一書中。雖然每個具體的原則在此之前已經存在,但羅伯特·C·馬丁將它們整合在一起,并由邁克爾·C·費瑟斯(Michael C. Feathers)建議將其命名為“SOLID”,形成了我們今天熟知的形式。

羅伯特·C·馬丁是敏捷開發和清潔代碼領域的傳奇人物,他對全球程序員社區產生了深遠的影響。

在了解這些原則之前,我們需要先了解 敏捷(Agile)的概念。只有理解了敏捷,才能明白這本書為何如此具有革命性,甚至被視為程序員的“圣經”。

敏捷宣言與SOLID的關系

2001年,程序員們發表了《敏捷宣言》。這份宣言強調了編程中的哲學價值,但并沒有提供具體的執行方法。羅伯特·C·馬丁在《敏捷軟件開發:原則、模式與實踐》中提供了具體的實施方案,將敏捷的“哲學”轉化為“方法論”。這一成果幫助敏捷從理論走向了實際標準。

在初版中,SRP、OCP、LSP、ISP、DIP是分別介紹的,之后在這本書出版后,根據邁克爾·C·費瑟斯(Michael C. Feathers)的建議,這些原則被命名為“SOLID”。

(具體時間線推測大約在2004年左右正式命名為“SOLID”)

這些原則被描述為敏捷設計的方法論。

敏捷原則并不僅僅是快速編碼,而是以具備長期可維護性和適應變化能力的設計為目標進行說明的。

因此,遵循SOLID原則對內化敏捷設計方法論有很大的幫助。

之后,羅伯特·馬丁(Uncle Bob)根據邁克爾·C·費瑟斯的建議,在博客和演講中將其命名為“SOLID”,形成了我們熟知的“SOLID原則”。

(“SOLID”有固體、堅固的意思,所以這里也包含了一種文字游戲的趣味)

第七章總結

通過代碼中的設計異味(Smells) ,如僵化性(Rigidity)、脆弱性(Fragility)、不流動性(Immobility)、粘滯性(Viscosity)等問題識別問題,并通過漸進式的改進逐步完善設計。這種改進并非一開始就追求完美,而是通過反復重構來優化,而SOLID原則正是這一過程中的實際方法論。

書中提到的“有異味的代碼”的特征如下:
僵化性(Rigidity)

定義

  • 軟件難以修改。也就是說,代碼變得僵硬,當試圖修改某一部分時,
  • 修改會對系統的其他多個部分產生影響,導致需要比預期更多的工作量。

特點

  • 代碼高度耦合(Tightly Coupled),小的改動會引發連鎖修改需求。
  • 難以根據需求變化進行調整。

示例

  • 修改一個類的方法時,發現需要同時修改調用它的數十個其他類。
  • 更改數據庫模式時,需要對UI、業務邏輯、數據訪問層進行全面修改。

問題原因 :高耦合度(High Coupling)和低內聚性(Low Cohesion)。

解決方法 :通過SOLID中的SRP(單一職責原則)和DIP(依賴倒置原則)降低模塊間的耦合度,減少修改對其他部分的影響。

脆弱性(Fragility)

定義

  • 軟件容易崩潰或出現錯誤的狀態。
  • 修改某一部分時,意想不到的其他部分出現問題,或者系統變得不穩定。

特點

  • 修改代碼后頻繁出現Bug。
  • 在與修改部分無直接關聯的區域出現錯誤。

示例

  • 修改支付模塊后,登錄系統突然無法正常工作。
  • 改進某個方法的邏輯后,使用該方法的其他模塊發生運行時錯誤。

問題原因 :不恰當的繼承使用(例如違反LSP)或依賴管理失敗。

解決方法 :遵守LSP(里氏替換原則)確保繼承結構的穩定性,并通過ISP(接口隔離原則)移除不必要的依賴。

不流動性(Immobility)

定義

  • 軟件組件難以復用或移動的狀態。
  • 當嘗試將特定模塊或代碼用于其他項目或上下文時,由于過度依賴,難以分離或無法復用。

特點

  • 代碼過于緊密地綁定到特定環境。
  • 如果要復用,需要進行大量修改。

示例

  • 數據庫查詢邏輯與UI代碼糾纏在一起,導致無法在其他項目中單獨復用查詢邏輯。
  • 依賴特定硬件的代碼無法在其他平臺上運行。

問題原因 :模塊間高耦合度和缺乏抽象。

解決方法 :通過DIP(依賴倒置原則)設計為依賴抽象而非具體實現,并通過OCP(開閉原則)創建可復用的結構。

粘滯性(Viscosity)

定義

  • 軟件開發環境或代碼使任務變得困難且緩慢的狀態。
  • 粘滯性高意味著“正確的方式”比“錯誤的方式”更難操作。

特點

  • 主要分為兩種形式:
    • 軟件粘滯性(Viscosity of the Software) :代碼本身難以維護或難以應用良好設計的情況。
    • 環境粘滯性(Viscosity of the Environment) :構建、測試、部署等開發環境效率低下,導致工作速度變慢。

示例

  • 復制粘貼添加代碼比重構更容易(軟件粘滯性)。
  • 構建時間過長,導致代碼修改后驗證變慢(環境粘滯性)。

問題原因 :未遵守設計原則,開發流程復雜。

解決方法 :通過SRP保持代碼簡潔性,并通過持續重構和自動化測試/構建環境降低粘滯性。

不過,僅靠SOLID原則并不能消除所有設計異味。

它只是一個高質量的指導方針,這些代碼異味還取決于個人的架構哲學、主觀判斷以及實際需求。

例如,典型的“霰彈槍手術(Shotgun Surgery)”問題僅靠SOLID原則很難解決。

“霰彈槍手術”的原因通常是責任分配過多。

“霰彈槍手術”:這是一種反模式(Anti-Pattern),雖然屬于錯誤代碼的案例,但經常出現。通常,“霰彈槍手術”是因為責任分散過多而導致的問題。

SOLID與敏捷(Agile)如何關聯?
  • SRP 減少代碼的僵化性(Rigidity)。
  • OCP 在不修改現有代碼的情況下擴展功能,從而降低代碼間的粘滯性(Viscosity)。
  • LSP 保證繼承結構的安全性,從而減少脆弱性(Fragility)。
  • DIP ISP 降低耦合度,從而減少不流動性(Immobility)。

(img ref:https://www.instagram.com/techwithisha/reel/C1Ws1ZDt8_j/)

SOLID原則?

那么,我們已經了解了上述問題的原因,現在讓我們來詳細探討一下SOLID原則。

我對所有智能思維特征的理解

這指的是深入研究某一主題特定方面的態度。

這種研究不僅是為了保持該方面的一致性,同時也意識到自己所處理的內容只是整體的一部分。

我們知道程序必須正確運行,

因此我們可以從這個角度研究程序。

此外,我們也知道程序必須高效,

這一分析可以在其他時間單獨進行。

有時,我們會思考程序是否真的必要,如果是,為什么。

然而,同時處理這些不同方面并不會帶來任何好處,反而只會造成干擾。

我稱之為“關注點分離(Separation of Concerns)”,

雖然無法完全實現,但這是有效整理思路的唯一方法。

當我說“專注于某一方面”時,

這并不意味著忽略其他方面。

相反,這意味著從某個特定方面的角度來看,其他方面暫時并不重要。

也就是說,這是一種既能專注于一件事,又能同時考慮多件事的思維方式。

—Edsger Wybe Dijkstra(On the role of scientific thought, 1982)

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

提出者 : 羅伯特·C·馬丁(Robert C. Martin)

定義

  • “一個類應該只有一個職責。” 也就是說,類應該只有一個修改的理由。

意義

  • 如果一個類承擔多個角色,一個角色的變化會影響其他角色,導致僵化性和脆弱性增加。
  • 分離職責可以使代碼更簡單,維護更容易。

示例代碼

using System;
using System.Data.SqlClient;
public class Employee_BadExample // 通過類名標明 BadExample
{public string Name { get; set; }public double BaseSalary { get; set; }public string Department { get; set; }private SqlConnection _dbConnection; // Employee 類竟然還負責數據庫連接!public Employee_BadExample(string name, double baseSalary, string department, SqlConnection dbConnection){Name = name;BaseSalary = baseSalary;Department = department;_dbConnection = dbConnection; // Employee 類接受數據庫連接對象}public double CalculateSalary(){"""計算薪資的方法 (假設不同部門的獎金比例不同)"""double bonusRate = 0;if (Department == "Sales"){bonusRate = 0.1;}else if (Department == "Marketing"){bonusRate = 0.05;}return BaseSalary * (1 + bonusRate);}public void SaveToDatabase(){"""將員工信息存儲到數據庫的方法"""double salary = CalculateSalary(); // 計算薪資邏輯竟然也在 Employee 內!try{_dbConnection.Open();SqlCommand command = new SqlCommand("INSERT INTO Employees (Name, Salary, Department) VALUES (@Name, @Salary, @Department)", _dbConnection);command.Parameters.AddWithValue("@Name", Name);command.Parameters.AddWithValue("@Salary", salary);command.Parameters.AddWithValue("@Department", Department);command.ExecuteNonQuery();}catch (Exception ex){Console.WriteLine("數據庫存儲錯誤: " + ex.Message);}finally{_dbConnection.Close();}}
}
public class BadExample_Program // 使用 BadExample 的主程序類
{public static void Main(string[] args){// 錯誤的示例:Employee_BadExample 類承擔了太多職責!SqlConnection dbConn = null; // 需替換為實際數據庫連接對象 (此處用 null 代替)Employee_BadExample employee = new Employee_BadExample("???", 3000000, "Sales", dbConn);employee.SaveToDatabase(); // Employee_BadExample 既計算薪資,又保存數據庫!}
}

錯誤示例 : Employee類同時處理薪資計算和數據庫存儲,薪資邏輯變化會影響數據庫代碼。

using System;
using System.Data.SqlClient;
public class Employee // Employee 類僅負責數據
{public string Name { get; set; }public double BaseSalary { get; set; }public string Department { get; set; }public Employee(string name, double baseSalary, string department){Name = name;BaseSalary = baseSalary;Department = department;}
}
public class SalaryCalculator // 負責薪資計算的類
{public double CalculateSalary(Employee employee) // 接收 Employee 對象作為參數{"""計算薪資的方法"""double bonusRate = 0;if (employee.Department == "Sales"){bonusRate = 0.1;}else if (employee.Department == "Marketing"){bonusRate = 0.05;}return employee.BaseSalary * (1 + bonusRate);}
}
public class EmployeeRepository // 負責數據庫存儲的類
{private SqlConnection _dbConnection;public EmployeeRepository(SqlConnection dbConnection){_dbConnection = dbConnection;}public void Save(Employee employee, double salary) // 接收 Employee 對象和計算后的薪資{"""將員工信息存儲到數據庫的方法"""try{_dbConnection.Open();SqlCommand command = new SqlCommand("INSERT INTO Employees (Name, Salary, Department) VALUES (@Name, @Salary, @Department)", _dbConnection);command.Parameters.AddWithValue("@Name", employee.Name);command.Parameters.AddWithValue("@Salary", salary);command.Parameters.AddWithValue("@Department", employee.Department);command.ExecuteNonQuery();}catch (Exception ex){Console.WriteLine("數據庫存儲錯誤: " + ex.Message);}finally{_dbConnection.Close();}}
}
public class GoodExample_Program // 使用 GoodExample 的主程序類
{public static void Main(string[] args){// 正確的示例:每個類只承擔單一職責!SqlConnection dbConn = null; // 需替換為實際數據庫連接對象Employee employee = new Employee("???", 3500000, "Marketing");SalaryCalculator calculator = new SalaryCalculator(); // 創建負責薪資計算的對象double salary = calculator.CalculateSalary(employee);EmployeeRepository repository = new EmployeeRepository(dbConn); // 創建負責數據庫存儲的對象repository.Save(employee, salary);}
}

正確示例 : 將其分為SalaryCalculatorEmployeeRepository

優點 : 提高代碼的內聚性(Cohesion),降低耦合度(Coupling)。
相關設計異味 : 僵化性、粘滯性緩解。

(Clean Coder Blog)

歷史背景 : 根據羅伯特·C·馬丁博客所述,SRP起源于大衛·L·帕納斯的模塊分解和戴克斯特拉的關注點分離概念。
結合當時編程社區中流行的耦合與內聚概念,最終形成了SRP。

正如羅伯特·C·馬丁在博客中所說,SRP是關于人的。
現實中,軟件會隨著企業或組織的需求而變化,因此每個模塊只負責單一業務功能,以便于明確哪個團隊負責修改該功能。

2. 開閉原則(OCP, Open/Closed Principle)

提出者 : 貝特朗·邁耶(Bertrand Meyer)

定義

  • “軟件實體(類、模塊等)應對擴展開放,對修改關閉。”

意義

  • 在不修改現有代碼的情況下添加新功能。
  • 使用抽象(接口、抽象類)和多態性來實現。

示例

using System;
public class PaymentProcessor_BadExample // 通過類名標明 BadExample
{public void ProcessPayment(string paymentMethod, double amount){"""根據支付方式處理支付的方法 (大量使用 if-else 語句)"""if (paymentMethod == "Card"){// 處理信用卡支付邏輯Console.WriteLine($"使用信用卡支付 {amount} 元");}else if (paymentMethod == "Cash"){// 處理現金支付邏輯Console.WriteLine($"使用現金支付 {amount} 元");}else if (paymentMethod == "MobilePay") // 新增支付方式!必須修改代碼!{// 處理移動支付邏輯Console.WriteLine($"使用移動支付 {amount} 元");}else{Console.WriteLine("不支持的支付方式");}}
}
public class BadExample_Program // 使用 BadExample 的主程序類
{public static void Main(string[] args){// 錯誤的示例: PaymentProcessor_BadExample 違反開放-封閉原則 (OCP),無法輕易擴展!PaymentProcessor_BadExample processor = new PaymentProcessor_BadExample();processor.ProcessPayment("Card", 10000);processor.ProcessPayment("Cash", 5000);processor.ProcessPayment("MobilePay", 7000); // 使用新的支付方式}
}

錯誤示例 : 每次向PaymentProcessor類添加新的支付方式(如卡支付、現金支付)時,都需要修改if-else條件。

using System;
//  IPayment 接口: 適用于各種支付方式的通用接口
public interface IPayment
{void ProcessPayment(double amount);
}
//  信用卡支付類 (實現 IPayment)
public class CardPayment : IPayment
{public void ProcessPayment(double amount){Console.WriteLine($"[信用卡支付] 付款 {amount:N0} 元 完成");}
}
//  現金支付類 (實現 IPayment)
public class CashPayment : IPayment
{public void ProcessPayment(double amount){Console.WriteLine($"[現金支付] 付款 {amount:N0} 元 完成");}
}
//  移動支付類 (實現 IPayment)
public class MobilePayPayment : IPayment
{public void ProcessPayment(double amount){Console.WriteLine($"[移動支付] 付款 {amount:N0} 元 完成");}
}
//  支付處理類: 符合開放-封閉原則 (OCP)
public class PaymentProcessor
{private readonly IPayment _paymentMethod;//  通過構造函數注入支付方式 (可應用依賴注入 DI)public PaymentProcessor(IPayment paymentMethod){_paymentMethod = paymentMethod ?? throw new ArgumentNullException(nameof(paymentMethod));}public void Process(double amount){_paymentMethod.ProcessPayment(amount);}
}
public class Program
{public static void Main(){// 創建各種支付方式對象var cardPayment = new CardPayment();var cashPayment = new CashPayment();var mobilePayPayment = new MobilePayPayment();// 符合 OCP: 新增支付方式時,無需修改 PaymentProcessor 代碼var processor1 = new PaymentProcessor(cardPayment);processor1.Process(10000);var processor2 = new PaymentProcessor(cashPayment);processor2.Process(5000);var processor3 = new PaymentProcessor(mobilePayPayment);processor3.Process(7000);}
}

正確示例 : 創建IPayment接口,并通過CardPaymentCashPayment類進行擴展。

優點 : 維持現有代碼的穩定性,靈活應對新需求。
相關設計異味 : 僵化性、不流動性緩解。

出處 : 出自《面向對象軟件構造》(Object-Oriented Software Construction, 1988),第2章“模塊化”部分。

(Data Abstraction and Hierarchy) (1987, OOPSLA )

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

提出者 : 芭芭拉·利斯科夫(Barbara Liskov)

定義

  • “子類應能在不干擾父類行為的情況下替代父類。”
  • 也就是說,在程序中用子類型替換父類型時,程序仍能正常運行。

意義

  • 在繼承關系中,子類不應違反父類的契約(Contract)。
  • 這是安全使用多態性的原則。

示例

public class Bird
{public virtual void Fly() => Console.WriteLine("鳥在飛翔。");
}
public class Penguin : Bird
{public override void Fly() //  企鵝不能飛!{throw new NotImplementedException("企鵝無法飛行。");}
}
public class Program
{public static void MakeBirdFly(Bird bird){bird.Fly(); //  如果傳入的是 Penguin 對象,則會拋出異常!}static void Main(){Bird myBird = new Penguin();MakeBirdFly(myBird); //  可能導致程序崩潰}
}

錯誤示例 : Bird類有Fly()方法,而Penguin子類忽略或拋出異常。


//  將 Bird 抽象化,不允許直接使用
public abstract class Bird { } 
//  定義飛行接口
public interface IFlyable
{void Fly();
}
//  麻雀類 (實現 IFlyable 接口)
public class Sparrow : Bird, IFlyable
{public void Fly() => Console.WriteLine("麻雀在飛翔。");
}
//  企鵝類 (不實現 IFlyable 接口,表示不能飛)
public class Penguin : Bird { } 
public class Program
{public static void MakeBirdFly(IFlyable bird){bird.Fly();}static void Main(){IFlyable sparrow = new Sparrow();MakeBirdFly(sparrow); //  正常運行}
}

正確示例 : 將Bird分為FlyingBirdWalkingBird,使Penguin不需要實現Fly()

優點 : 確保繼承結構的穩定性和可預測性。
相關設計異味 : 脆弱性緩解。

背景 : 該原則源自1987年OOPSLA會議論文,論文討論了數據抽象和層次結構,為面向對象中的“繼承(Inheritance)”提供了哲學和實用的指導方針。

數據抽象是指隱藏程序中數據的內部實現,僅通過接口訪問。

歸根結底,這是一個如何更好地抽象現實問題的問題。例如,如果將哺乳動物定義為“有腿的生物”,那么鯨魚就難以被稱為哺乳動物。因此,如何恰當地進行抽象才是關鍵。

4. 接口隔離原則(ISP, Interface Segregation Principle)

提出者 : 羅伯特·C·馬丁(Robert C. Martin)

定義

  • “客戶端不應依賴于它不需要的接口。”
  • 也就是說,接口應盡可能小且具體。

意義

  • 設計只提供客戶端所需功能的接口,而不是大型通用接口。
  • 移除不必要的依賴以降低耦合度。

示例

using System;
// IWorker_BadExample 接口: 包含了太多功能 (違反 ISP)
public interface IWorker_BadExample
{void Work();   // 工作功能void Eat();    // 進食功能 - 但對 Robot 來說是不必要的!
}
// Robot_BadExample 類: 實現 IWorker_BadExample,必須強制實現不必要的 Eat() 方法
public class Robot_BadExample : IWorker_BadExample
{public void Work(){Console.WriteLine("機器人正在努力工作。");}public void Eat() //  機器人不需要進食,但仍然必須實現{// 機器人不吃飯,因此只能什么都不做,或者拋出異常Console.WriteLine("機器人無法進食。"); // 或者 throw new NotImplementedException();}
}
// HumanWorker_BadExample 類: 實現 IWorker_BadExample,正確地實現 Work() 和 Eat()
public class HumanWorker_BadExample : IWorker_BadExample
{public void Work(){Console.WriteLine("人類正在努力工作。");}public void Eat(){Console.WriteLine("人類正在吃午飯。");}
}
public class BadExample_Program // 使用 BadExample 的程序類
{public static void Main(string[] args){//  錯誤示例: Robot_BadExample 不應該有 Eat() 方法!IWorker_BadExample robot = new Robot_BadExample();robot.Work();robot.Eat(); // 機器人調用 Eat() 方法顯得很奇怪IWorker_BadExample human = new HumanWorker_BadExample();human.Work();human.Eat();}
}

錯誤示例 : IWorker接口包含Work()Eat()方法,導致Robot類需要實現不必要的Eat()方法。

using System;
// IWorkable 接口: 僅包含工作功能 (遵循 ISP)
public interface IWorkable
{void Work(); // 工作功能
}
//  IEatable 接口: 僅包含進食功能 (遵循 ISP)
public interface IEatable
{void Eat();  // 進食功能
}
// Robot_GoodExample 類: 僅實現 IWorkable 接口 (僅實現必要的功能)
public class Robot_GoodExample : IWorkable // 機器人只能工作
{public void Work(){Console.WriteLine("機器人高效地執行任務。");}// Eat() 方法未實現: 機器人不需要進食
}
//  HumanWorker_GoodExample 類: 實現 IWorkable 和 IEatable 接口 (擁有所有必要功能)
public class HumanWorker_GoodExample : IWorkable, IEatable // 人類既能工作,也能進食
{public void Work(){Console.WriteLine("人類創造性地工作。");}public void Eat(){Console.WriteLine("人類正在享受美味的午餐。");}
}
public class GoodExample_Program // 使用 GoodExample 的程序類
{public static void Main(string[] args){//  正確示例: Robot_GoodExample 只需要實現 IWorkable!IWorkable robot = new Robot_GoodExample(); // 機器人僅用作 IWorkable 類型robot.Work();// robot.Eat(); // Robot 未實現 IEatable,因此無法調用 Eat() 方法 (編譯錯誤)IWorkable humanWorker = new HumanWorker_GoodExample(); // HumanWorker 可用作 IWorkable 類型humanWorker.Work();IEatable humanEater = new HumanWorker_GoodExample(); // HumanWorker 也可用作 IEatable 類型humanEater.Eat();}
}

正確示例 : 將接口拆分為IWorkableIEatable,使Robot只需實現IWorkable

優點 : 提高代碼的靈活性和可重用性。
相關設計異味 : 脆弱性、粘滯性緩解。

出處 : 出自羅伯特·C·馬丁1996年的文章。

(1996 Robert C. Martin Essay)

5. 依賴倒置原則(DIP, Dependency Inversion Principle)

提出者 : 羅伯特·C·馬丁(Robert C. Martin)

定義

  • “高層模塊不應依賴于低層模塊,二者都應依賴于抽象。”
  • 此外,“不要依賴具體實現,而是依賴抽象。”

意義

  • 通過接口或抽象類減少模塊間的依賴。
  • 通過依賴注入(Dependency Injection)實現。

示例

using System;
// SqlDatabase 類: 具體數據庫的實現 (低級模塊,直接依賴于某個數據庫)
public class SqlDatabase_BadExample
{public void Save(string data){// 實際將數據存入 SqlDatabase 的邏輯 (省略實現)Console.WriteLine($"數據已存入 SqlDatabase: {data}");}
}
// OrderService_BadExample 類: 直接依賴 SqlDatabase (違反 DIP,屬于高級模塊)
public class OrderService_BadExample
{private SqlDatabase_BadExample _database; // 直接依賴于具體的 SqlDatabase 類!public OrderService_BadExample(){_database = new SqlDatabase_BadExample(); // OrderService 直接創建 SqlDatabase 實例}public void PlaceOrder(string orderData){// 訂單處理邏輯 (這里只是簡單地存儲數據)Console.WriteLine($"訂單處理中: {orderData}");_database.Save(orderData); // OrderService 直接調用 SqlDatabase 的 Save() 方法Console.WriteLine("訂單處理完成");}
}
public class BadExample_Program // 使用 BadExample 的程序類
{public static void Main(string[] args){//  錯誤示例: OrderService_BadExample 與 SqlDatabase 強耦合,難以擴展!OrderService_BadExample service = new OrderService_BadExample();service.PlaceOrder("客戶: 張三, 商品: 筆記本電腦");}
}

錯誤示例 : OrderService直接依賴于SqlDatabase,更換數據庫時需要修改代碼。

using System;
// 遵循 DIP (依賴倒置原則): OrderService 僅依賴 IDatabase 接口
public interface IDatabase
{void SaveOrder(string orderDetails);
}
// SqlDatabase 實現 IDatabase 接口
public class SqlDatabase : IDatabase
{public void SaveOrder(string orderDetails){Console.WriteLine($"[SqlDatabase] 訂單已保存: {orderDetails}");}
}
// MongoDatabase 實現 IDatabase 接口 (可以添加新的數據庫類型)
public class MongoDatabase : IDatabase
{public void SaveOrder(string orderDetails){Console.WriteLine($"[MongoDatabase] 訂單已保存: {orderDetails}");}
}
// OrderService 依賴于接口 (IDatabase),不依賴具體實現
public class OrderService
{private readonly IDatabase _database;// 依賴倒置原則 (DIP): OrderService 依賴接口,而不是具體實現public OrderService(IDatabase database){_database = database; // 依賴注入 (Dependency Injection)}public void PlaceOrder(string orderDetails){_database.SaveOrder(orderDetails); // 通過接口存儲訂單}
}
// OrderService 不再依賴特定數據庫 → 可以輕松切換數據庫
public class Program
{public static void Main(){// 在不使用 DI 容器的情況下,直接創建對象并注入依賴IDatabase sqlDatabase = new SqlDatabase();IDatabase mongoDatabase = new MongoDatabase();// 使用 SqlDatabase 的 OrderServiceOrderService orderService1 = new OrderService(sqlDatabase);orderService1.PlaceOrder("商品 A 訂單");// 使用 MongoDatabase 的 OrderServiceOrderService orderService2 = new OrderService(mongoDatabase);orderService2.PlaceOrder("商品 B 訂單");}
}

正確示例 : 創建IDatabase接口,OrderService依賴于接口,SqlDatabase作為具體實現。

優點 : 提高系統的靈活性和測試便利性。
相關設計異味 : 僵化性、不流動性緩解。

SOLID原則的整體意義

提高可維護性和擴展性,從而構建能夠靈活應對變化的軟件。

原則

定義

提出者

S - 單一職責原則 (SRP)

一個類應該只有一個職責。

羅伯特·C·馬丁 (2002)

O - 開閉原則 (OCP)

在不修改現有代碼的情況下擴展功能。

貝特朗·邁耶 (1988)

L - 里氏替換原則 (LSP)

子類應能替代父類。

芭芭拉·利斯科夫 (1987)

I - 接口隔離原則 (ISP)

客戶端不應依賴于它不需要的接口。

羅伯特·C·馬丁 (2002)

D - 依賴倒置原則 (DIP)

高層模塊不應依賴于低層模塊,而是依賴于抽象。

羅伯特·C·馬丁 (1996)

SOLID原則真的是絕對正確的答案嗎?

當然不是。SOLID只是一個指導方針。

示例:虛幻引擎中的Actor

虛幻引擎的Actor負責物理、碰撞、光照、網格渲染等大量任務。

也就是說,單個類承擔了太多的任務。那么,如果將這些任務分開,真的會變得方便嗎?完全不會。相反,分離的成本可能遠遠高于收益。

Actor是虛幻引擎的核心基礎類。如果為了遵循SRP(單一職責原則)而將其拆分,會對整個引擎產生影響。

那么,這是不是一個糟糕的設計呢?并不是。因為游戲是以對象為中心設計的,在這個單位下,這種設計是有充分理由的。

如果強行拆分,反而會導致在對象級別重新組合時需要付出巨大的成本。

LSP的情況

同樣地,LSP(里氏替換原則)也有例外。例如,在GUI框架中,當Button繼承自Widget時,是否必須保證父類的按鈕行為?

如果是這樣,反而會導致實際問題。在這種情況下,如果需要多樣化地設計按鈕樣式,可能會破壞設計的創造性。

因此,許多GUI框架(如C++的QT框架和GTK等)允許這樣的例外。

此外,由于接口帶來的開銷,有時選擇DOP(數據導向編程)而非OOP可能是更合適的。

雖然SOLID是面向對象編程(OOP)設計的一個很好的指導方針,但也需要了解OOP可能存在的性能限制和設計背景,并明確何時可以不遵守這些原則。

那么,什么時候可以違背SOLID原則?
  • SRP(單一職責原則) :如果功能具有很強的內聚性,嚴格遵守SRP可能導致類之間頻繁調用方法,從而引發性能開銷。
  • OCP(開閉原則) :雖然擴展頻繁是好事,但如果性能優先,修改可能比擴展更好。
  • LSP(里氏替換原則) :如果繼承結構簡單,則不需要強制遵守。
  • ISP(接口隔離原則) :如果接口過多,反而會導致混亂。
  • DIP(依賴倒置原則) :如果抽象化帶來了額外開銷,具體依賴可能是更好的選擇。

“假設維護你代碼的人是一個知道你住址的暴力精神病患者,那么你就應該以這種方式編寫代碼。”-John F. Woods(1991年)

SOLID的本質與實用性平衡

SOLID原則誕生于敏捷哲學,作為提升代碼可維護性和擴展性的強大工具,已經占據了重要地位。

然而,它并不是適用于所有情況的“銀彈”。

對于像虛幻引擎的Actor這樣具有強烈領域特性的場景,或者在GUI框架中需要發揮創造力時,又或者在性能至關重要的系統中,與其勉強遵循SOLID原則,不如選擇適合上下文的設計更為重要。

正如John F. Woods所說,“假設維護你代碼的人是一個知道你住址的暴力精神病患者”,這不僅僅是在提醒我們寫易讀的代碼。

其背后的真正含義是,無論在什么情況下,都要讓處理代碼的人能夠輕松理解并適應代碼。

SOLID只是實現這一目標的一種方式,而不是唯一的方式。有時,違反SRP保持一個整合的類,或者忽略DIP選擇具體的依賴,可能是防止“精神病患者”采取極端行動的實用選擇。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/72188.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/72188.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/72188.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

輕松實現語音生成:GPT-SoVITS V2整合包的遠程訪問操作詳解

文章目錄 前言1.GPT-SoVITS V2下載2.本地運行GPT-SoVITS V23.簡單使用演示4.安裝內網穿透工具4.1 創建遠程連接公網地址 5. 固定遠程訪問公網地址 前言 今天要給大家安利一個絕對能讓你大呼過癮的聲音黑科技——GPT-SoVITS!這款由花兒不哭大佬精心打造的語音克隆神…

Python線程池知多少

目錄 目標 Python版本 官方文檔 概述 線程池 實戰 創建線程池的基本語法 批量提交任務 生產者&消費者模型 目標 掌握線程池的基本概念和使用方法。 Python版本 Python 3.9.18 官方文檔 concurrent.futures — Launching parallel taskshttps://docs.python.org/3…

(轉)SpringBoot和SpringCloud的區別

(轉)SpringBoot和SpringCloud的區別:

中科大 計算機網絡組成原理 1.4 接入網和物理媒體 筆記

一、接入網核心功能與架構 ?核心作用? 接入網是連接用戶終端與核心網絡的橋梁,承擔用戶身份認證、帶寬分配、數據加密等功能,直接影響網絡服務的可靠性和用戶體驗。例如,杭州電視臺的數字人主播通過光纖專線實現零失誤新聞播報,…

阿里云音頻算法崗內推

1、視頻云直播、連麥,點播,短視頻,媒體生產與處理等服務相關的實時/非實時的音頻分析和處理; 2、音頻處理算法,包括多場景降噪、自動增益控制、回聲消除等; 3、音頻特效算法研發,包括變調變速…

如何使用DeepSeek輔助準備面試

前言 又到了金三銀四的時間點了。每年的這個時間點都會出現無數的機遇和機會,但是如何準備面試,應該準備哪些面試題,如何查漏補缺我們的技術面的短板,這是我們每次準備面試的時候,都會遇見的問題。在今年,…

如何流暢訪問github

1.傳輸數據原理 本地計算機通過本地網接入運營骨干網,經過DNS域名解析,將輸入的字符解析為要連接的真實IP地址,服務器返還一個數據包(github)給計算機 2.原因 DNS域名污染-DNS解析出現問題,導致訪問一個不存在的服務器 3.解決…

JPA屬性轉換器的使用與實例解析

在Java持久化框架中,JPA(Java Persistence API)為我們提供了強大的功能來操作數據庫。其中,屬性轉換器(Attribute Converter)是一個非常實用的特性,它允許我們將實體類中的屬性類型轉換為適合存…

AI數據分析:用DeepSeek做數據清洗

在當今數據驅動的時代,數據分析已成為企業和個人決策的重要工具。隨著人工智能技術的快速發展,AI 驅動的數據分析工具正在改變我們處理和分析數據的方式。本文將著重介紹如何使用 DeepSeek 進行數據清洗。 數據清洗是數據分析的基礎,其目的是…

rust學習~tokio的io

await Suspend execution until the result of a Future is ready. 暫停執行,直到一個 Future 的結果就緒。 .awaiting a future will suspend the current function’s execution until the executor has run the future to completion. 對一個 Future 使用 .awa…

騰訊2025年軟件測試面試題

以下是基于騰訊等一線互聯網公司軟件測試崗位的面試趨勢和技術要求,025年出現的軟件測試面試題。這些問題涵蓋了基礎知識、自動化測試、性能測試、安全測試、編程能力等多個方面,供參考和準備。 一、基礎知識 軟件測試的基本概念

數據結構(陳越,何欽銘) 第四講 樹(中)

4.1 二叉搜索樹 4.1.1 二叉搜索樹及查找 Position Find(ElementTyoe X,BinTree BST){if(!BST){return NULL;}if(X>BST->Data){return Find(X,BST->Right)}else if(X<BST->Data){return Find(X,BST->Left)}else{return BST;} } Position IterFind(ElementTyp…

GEE學習筆記 28:基于Google Earth Engine的Landsat8纓帽變換土壤指數反演——亮度、綠度與濕度分量的提取

1.纓帽變換介紹 纓帽變換(Tasseled Cap Transformation,TCT),也稱為纓帽特征空間或纓帽系數,是一種用于遙感圖像分析的線性變換方法。它最初由美國農業部的研究人員E. Kauth和G. Thomas在1976年提出,用于增強陸地衛星(Landsat)圖像中的特定地表特征,如植被、土壤和城市…

【現代Web布局與動畫技術:卡片組件實戰分享】

&#x1f4f1; 現代Web布局與動畫技術&#xff1a;卡片組件實戰分享 &#x1f680; 引言 &#x1f31f; 在過去的開發過程中&#xff0c;我們共同實現了一個功能豐富的卡片組件&#xff0c;它不僅美觀&#xff0c;還具有交互性和響應式設計。這篇文章將分享這個組件背后的技術…

學習路之PHP --TP6異步執行功能 (無需安裝任何框架)

學習路之PHP --異步執行功能 &#xff08;無需安裝任何框架&#xff09; 簡介一、工具類二、調用三、異步任務的操作四、效果&#xff1a; 簡介 執行異步任務是一種很常見的需求&#xff0c;如批量發郵箱&#xff0c;短信等等執行耗時任務時&#xff0c;需要程序異步執行&…

STM32之影子寄存器

預分頻寄存器計數到一半的時候&#xff0c;改變預分頻值&#xff0c;此時不會立即生效&#xff0c;會等到計數完成&#xff0c;再從影子寄存器即預分頻緩沖器里裝載修改的預分頻值。 如上圖&#xff0c;第一行是內部時鐘72M&#xff0c;第二行是時鐘使能&#xff0c;高電平啟動…

Deepseek API接入IDE【VSCode Cline Cursor ChatBox Deepseek deepseek-reasoner】

本文解決以下疑難雜癥: 使用deepseek的最新接模型接入ide 使用deepseek的最新接模型接入vscode 使用deepseek的最新接模型接入vscode中的Cline 使用deepseek的最新接模型接入Cline 使用deepseek的最新接模型接入ChatBox 使用cursor接入Deepseek官方的的deepseek-reasoner…

微信小程序讀取寫入NFC文本,以及NFC直接啟動小程序指定頁面

一、微信小程序讀取NFC文本(yyy優譯小程序實現),網上有很多通過wx.getNFCAdapter方法來監聽讀取NFC卡信息,但怎么處理讀取的message文本比較難找,現用下面方法來實現,同時還解決幾個問題,1、在回調方法中this.setData不更新信息,因為this的指向問題,2、在退出頁面時,…

在Linux桌面上創建Idea啟動快捷方式

1、在桌面新建idea.desktop vim idea.desktop [Desktop Entry] EncodingUTF-8 NameIntelliJ IDEA CommentIntelliJ IDEA Exec/home/software/idea-2021/bin/idea.sh Icon/home/software/idea-2021/bin/idea.svg Terminalfalse TypeApplication CategoriesApplication;Developm…

VUE2生命周期頁面加載順序

使用 Vue CLI 4.5 運行 vue create myvue 創建項目&#xff0c;并通過 npm run serve 運行后&#xff0c;會生成一個標準的 Vue 項目目錄結構。以下是生成目錄的詳細說明&#xff0c;以及運行 localhost:8080 后 Vue 頁面的加載順序。 1. 生成目錄結構 運行 vue create myvue …