文章目錄
- 深入理解 SOLID:用對原則,別把簡單問題搞復雜
- SOLID 原則概覽
- 1. 單一職責原則(SRP)
- 2. 開閉原則(OCP)
- 3. 里氏替換原則(LSP)
- 4. 接口隔離原則(ISP)
- 5. 依賴反轉原則(DIP)
- 原則之間的聯系與平衡
- 案例
- 一、單一職責原則(SRP)—訂單處理模塊拆分
- 二、開閉原則(OCP)—優惠策略引擎
- 三、里氏替換原則(LSP)—圖表渲染組件
- 四、接口隔離原則(ISP)—外部服務集成
- 五、依賴反轉原則(DIP)—倉儲層設計
- 案例共性與最佳實踐
深入理解 SOLID:用對原則,別把簡單問題搞復雜
在面向對象編程的世界里,SOLID 原則幾乎是每個程序員最熟悉的五個字母組合——但也是最容易被“濫用”或“誤用”的設計準則。
很多同學往往將每一條原則孤立地、機械地應用,結果往往制造出十幾二十個冗余類,把原本簡單的需求復雜化。正如“有了錘子,就到處找釘子”,在并不必要的時候,你硬要用上 SOLID,就很可能把本該一刀搞定的小活兒,變成一場大型重構。
下面,我們就帶著“如何正確理解與應用”這個目標,一起來復盤 SOLID 五條原則的來龍去脈。
SOLID 原則概覽
2000 年,Robert C. Martin 在論文《設計原理和設計模式》中首次提出 SOLID 概念。過去二十年里,這五條原則幫助我們構建了更易維護、可擴展的系統:
- Single Responsibility Principle (SRP):單一職責原則
- Open–Closed Principle (OCP):開閉原則
- Liskov Substitution Principle (LSP):里氏替換原則
- Interface Segregation Principle (ISP):接口隔離原則
- Dependency Inversion Principle (DIP):依賴反轉原則
其核心價值在于——當團隊規模擴大、多人協作時,我們需要低耦合、高內聚、可替換的模塊。
1. 單一職責原則(SRP)
定義:一個類(或模塊)應該只有一個“引起它變化的原因”。
誤區:常被簡單理解為“一個類只做一件事”、“一個接口只實現一次”“寫好不能動”……而忘記“職責=變化的原因”這一核心。
示例:
public class Book {private String title, author, text;public String replaceWord(String word){ /*…*/ }public boolean containsWord(String word){ /*…*/ }public void print(){ /*…*/ } // ← 新增的打印責任public void read(){ /*…*/ } // ← 新增的閱讀責任 }
當打印邏輯變化時,你得修改
Book
,當閱讀流程變化時,又要修改它——職責不唯一,違反 SRP。
正確做法:抓住“職責”的邊界,職責可由多個類共同完成,但要保證各自變化原因單一;例如把打印與閱讀邏輯拆到 BookPrinter
、BookReader
。
2. 開閉原則(OCP)
定義:對擴展開放,對修改封閉。
誤區:把它當作業務代碼里的“金科玉律”,不管成本高低都要“零修改”;結果往往產生一大堆空殼類。
示例:Spring JDBC 的
AbstractDataSource
,通過繼承來擴展讀寫分離策略,而不修改框架源碼,即是 OCP 在框架層面的典型應用。
public abstract class Demo extends AbstractDataSource {private int readDsSize;@Overridepublic Connection getConnection() throws SQLException {return this.determineTargetDataSource().getConnection();}@Overridepublic Connection getConnection(String username, String password) throws SQLException {return this.determineTargetDataSource().getConnection(username, password);}protected DataSource determineTargetDataSource() {if (determineCurrentLookupKey() && this.readDsSize > 0){//讀庫做負載均衡(從庫)return this.loadBalance();} else {//寫庫使用主庫return this.getResolvedMasterDataSource();}}protected abstract boolean determineCurrentLookupKey();//其他代碼省略}
思考:在業務代碼里,需求迭代快,直接修改往往更高效;在框架、類庫或架構層面,才更有必要遵循 OCP,以減少對核心組件的侵入式改動。
3. 里氏替換原則(LSP)
定義:子類必須能夠替換父類,并保證行為一致性。
意義:保證多態下的可靠性,讓調用者無需感知具體子類,就能正確工作。
示例:自定義 Spring 的
PropertyEditorSupport
,遵循基類契約即可插入各種屬性編輯器,URL 參數解析也能“無感”替換。
比如,Spring 中提供的自定義屬性編輯器,可以解析 HTTP 請求參數中的自定義格式進行綁定并轉換為格式輸出。只要遵循基類(PropertyEditorSupport)的約束定義,就能為某種數據類型注冊一個屬性編輯器。我們先定義一個類 DefineFormat,具體代碼如下:
public class DefineFormat{private String rawStingFormat;private String uid;private String toAppCode;private String fromAppCode;private Sting timestamp;// 省略構造函數和get, set方法
}
然后,創建一個 Restful API 接口,用于輸入自定義的請求 URL。
@GetMapping(value = "/api/{d-format}",
public DefineFormat parseDefineFormat (@PathVariable("d-format") DefineFormat defineFormat) {return defineFormat;
}
接下來,創建 DefineFormatEditor,實現輸入自定義字符串,返回自定義格式 json 數據。
public class DefineFormatEditor extends PropertyEditorSupport {//setAsText() 用于將String轉換為另一個對象@Overridepublic void setAsText(String text) throws IllegalArgumentException {if (StringUtils.isEmpty(text)) {setValue(null);} else {DefineFormat df = new DefineFormat();df.setRawStingFormat(text);String[] data = text.spilt("-");if (data.length == 4) {df.setUid(data[0]);df.setToAppCode(data[1]);df.setFromAppCode(data[2]);df.setTimestamp(data[3]);setValue(df);} else {setValue(null);}}}//將對象序列化為String時,將調用getAsText()方法@Overridepublic String getAsText() {DefineFormat defineFormat= (DefineFormat) getValue();return null == defineFormat ? "" : defineFormat.getRawStingFormat();}
}
最后,輸入 url: /api/dlewgvi8we-toapp-fromapp-zzzzzzz
,返回響應。
{"rawStingFormat:"dlewgvi8we-toapp-fromapp-zzzzzz","uid:"dlewgvi8we","toAppCode":"toapp","fromAppCode":"fromapp","message":"zzzzzzz"
}
使用里氏替換原則(LSP)的本質就是通過繼承實現多態行為,這在面向對象編程中是非常重要的一個技巧,對于提高代碼的擴展性是很有幫助的。
要點:不僅要繼承接口簽名,還要遵守合同(前置條件不變、后置條件不減弱、異常行為不變化)。
4. 接口隔離原則(ISP)
定義:多個特定客戶端接口勝過一個通用接口。
誤區:只看單個接口中的方法數量,不考慮系統整體職責劃分。
示例:
interface ICRUD<T> { add(); update(); delete(); query(); sync(); }
當大多數業務并不需要
sync()
時,就被“強迫”實現,違反 ISP。正確的做法是拆分:interface ICRUD<T> { add(); update(); delete(); query(); } interface ISync { sync(); }
建議:整體系統層面思考職責,避免“一個接口—一個實現”式的過度拆分。
5. 依賴反轉原則(DIP)
定義:高層模塊不依賴底層模塊,二者都依賴抽象;抽象不依賴細節,細節依賴抽象。
內涵:關注“好的抽象”——既能覆蓋共性,又能靈活替換實現。
示例:JDBC 接口就是對關系型數據庫讀寫操作的正確抽象,底層可插拔各種數據庫驅動。
啟示:多用接口/抽象類定義契約,把實現細節隱藏在抽象之后;但切忌為了“抽象”而抽象,仍要結合業務場景。
原則之間的聯系與平衡
- SRP 是基礎:唯有職責清晰,才有更好的擴展(OCP)、替換(LSP)、隔離(ISP)和抽象(DIP)能力。
- OCP 是歸宿:真正做到修改關閉,需要在設計時結合 SRP、ISP、DIP 等原則為目標。
- DIP 更高層次:指導我們在架構級別“分離與替換”——借助抽象解耦模塊。
- 取舍:過度拆分會違背 KISS 原則。最簡單的折中方法是——控制接口數量,抽象通用職責,避免“一接口—一實現”的刻板模式。
專家:軟件工程 > 面向對象設計專家
要求:VERBOSITY?=?V=2(詳細),結合真實項目案例,分享 SOLID 原則在業務系統中的落地實踐,語言為中文。
計劃
- 簡要說明選取案例的背景和項目類型
- 按原則分類,每個案例包含:項目背景、問題痛點、SOLID 原則應用、效果與代碼片段
- 總結案例共性與最佳實踐建議
案例
一、單一職責原則(SRP)—訂單處理模塊拆分
項目背景
在某電商平臺的后臺服務中,OrderService
類同時負責訂單校驗、庫存扣減、支付調用、通知發送等多項功能,業務不斷膨脹,迭代時常因小改動引發連鎖故障。
問題痛點
- 一處變動可能影響多個流程,回歸測試成本高
- 類方法過長、維護困難,責任邊界不清晰
SRP 應用
-
拆分職責
OrderValidator
:只做訂單合法性校驗StockManager
:只做庫存預扣與回滾PaymentProcessor
:只負責與支付網關交互NotificationSender
:只負責訂單狀態變更通知
-
組合調用
public class OrderService {private final OrderValidator validator;private final StockManager stockManager;private final PaymentProcessor paymentProcessor;private final NotificationSender notifier;public void placeOrder(Order order) {validator.validate(order);stockManager.reserve(order);paymentProcessor.pay(order);notifier.send(order);} }
-
效果
- 各模塊職責清晰,單元測試覆蓋率提升至 90%
- 修改通知邏輯時,無需回歸庫存或支付流程
二、開閉原則(OCP)—優惠策略引擎
項目背景
促銷活動層出不窮,初期將 DiscountService
寫成多重 if-else
,每次上線新活動都要改這個類,風險極高。
問題痛點
- 修改封閉,新增促銷需頻繁改動原有代碼
- 條件分支難以維護,代碼臃腫
OCP 應用
-
抽象策略接口
public interface DiscountStrategy {BigDecimal calculate(Order order); }
-
各活動實現
@Component public class BlackFridayStrategy implements DiscountStrategy { /*…*/ }@Component public class NewUserStrategy implements DiscountStrategy { /*…*/ }
-
策略注冊與調用
@Component public class DiscountService {private final List<DiscountStrategy> strategies;public BigDecimal apply(Order order) {return strategies.stream().filter(s -> s.supports(order)).map(s -> s.calculate(order)).reduce(BigDecimal.ZERO, BigDecimal::add);} }
-
效果
- 新增策略只需編寫一個類并注入,無需改動
DiscountService
- 代碼體量更易擴展,回歸風險大幅降低
- 新增策略只需編寫一個類并注入,無需改動
三、里氏替換原則(LSP)—圖表渲染組件
項目背景
在后臺統計系統中,需要渲染不同類型的圖表(折線圖、柱狀圖、餅圖)。最初用 Chart
抽象類配合 if (type)
邏輯,后來改用繼承。
問題痛點
- 部分子類沒有實現所有方法,導致運行時拋出
UnsupportedOperationException
- 修改父類抽象方法會破壞部分子類行為
LSP 應用
-
精煉抽象
public interface ChartRenderer {void render(DataSet data); }
-
具體子類全力支持契約
public class LineChartRenderer implements ChartRenderer { /*…*/ } public class PieChartRenderer implements ChartRenderer { /*…*/ }
-
渲染調用無需分支
rendererMap.get(type).render(data);
-
效果
- 所有子類都能安全替換接口
- 后續新增
RadarChartRenderer
無需改動核心邏輯
四、接口隔離原則(ISP)—外部服務集成
項目背景
一套 CRM 系統需要對接多家短信、郵件、推送服務,最初定義一個 MessagingClient
接口,包含 sendSms
、sendEmail
、sendPush
,導致集成方只需郵件時也要實現短信、推送方法。
問題痛點
- 實現類方法樁多,代碼臃腫
- 不同服務方復用率低
ISP 應用
-
拆分接口
public interface SmsClient { void sendSms(SmsMessage msg); } public interface EmailClient { void sendEmail(Email msg); } public interface PushClient { void sendPush(PushMessage msg); }
-
各接入實現各自接口
public class TwilioSmsClient implements SmsClient { /*…*/ } public class SendGridEmailClient implements EmailClient { /*…*/ }
-
按需注入
@Service public class NotificationService {private final SmsClient sms;private final EmailClient email;public void notifyOrderCreated(Order o) {sms.sendSms(...);email.sendEmail(...);} }
-
效果
- 避免“被迫”實現無關方法
- 接口職責更聚焦,單元測試更簡潔
五、依賴反轉原則(DIP)—倉儲層設計
項目背景
某金融系統最初直接在業務層 LoanService
中 new JdbcLoanDao()
,測試時需要配合真實數據庫,耦合度高。
問題痛點
- 測試難以模擬,業務層依賴底層實現
- 更換存儲方式需改動業務層
DIP 應用
-
抽象 DAO 接口
public interface LoanRepository {Loan findById(String id);void save(Loan loan); }
-
業務層依賴接口
public class LoanService {private final LoanRepository repo;public LoanService(LoanRepository repo){ this.repo = repo; }// … 調用 repo 方法 }
-
底層實現注入
@Repository public class JdbcLoanRepository implements LoanRepository { /*…*/ }
-
效果
- 單元測試可注入內存或 Mock 實現
- 切換到 JPA 或其它存儲無業務層改動
案例共性與最佳實踐
- 先識別“變化點”,再拆分職責或抽象接口。
- 不要為了原則而原則,關注業務痛點與演進成本。
- 測試驅動設計(TDD) 有助于發現違反 SOLID 的耦合點。
- KISS 平衡:遵循 SOLID 的同時,也要兼顧代碼簡潔與團隊可讀性。