設計模式 - 面向對象原則:SOLID最佳實踐

文章目錄

  • 深入理解 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。

正確做法:抓住“職責”的邊界,職責可由多個類共同完成,但要保證各自變化原因單一;例如把打印與閱讀邏輯拆到 BookPrinterBookReader


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 接口就是對關系型數據庫讀寫操作的正確抽象,底層可插拔各種數據庫驅動。

啟示:多用接口/抽象類定義契約,把實現細節隱藏在抽象之后;但切忌為了“抽象”而抽象,仍要結合業務場景。


原則之間的聯系與平衡

  1. SRP 是基礎:唯有職責清晰,才有更好的擴展(OCP)、替換(LSP)、隔離(ISP)和抽象(DIP)能力。
  2. OCP 是歸宿:真正做到修改關閉,需要在設計時結合 SRP、ISP、DIP 等原則為目標。
  3. DIP 更高層次:指導我們在架構級別“分離與替換”——借助抽象解耦模塊。
  4. 取舍:過度拆分會違背 KISS 原則。最簡單的折中方法是——控制接口數量,抽象通用職責,避免“一接口—一實現”的刻板模式。

在這里插入圖片描述


專家:軟件工程 > 面向對象設計專家
要求:VERBOSITY?=?V=2(詳細),結合真實項目案例,分享 SOLID 原則在業務系統中的落地實踐,語言為中文。

計劃

  1. 簡要說明選取案例的背景和項目類型
  2. 按原則分類,每個案例包含:項目背景、問題痛點、SOLID 原則應用、效果與代碼片段
  3. 總結案例共性與最佳實踐建議

案例

一、單一職責原則(SRP)—訂單處理模塊拆分

項目背景
在某電商平臺的后臺服務中,OrderService 類同時負責訂單校驗、庫存扣減、支付調用、通知發送等多項功能,業務不斷膨脹,迭代時常因小改動引發連鎖故障。

問題痛點

  • 一處變動可能影響多個流程,回歸測試成本高
  • 類方法過長、維護困難,責任邊界不清晰

SRP 應用

  1. 拆分職責

    • OrderValidator:只做訂單合法性校驗
    • StockManager:只做庫存預扣與回滾
    • PaymentProcessor:只負責與支付網關交互
    • NotificationSender:只負責訂單狀態變更通知
  2. 組合調用

    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);}
    }
    
  3. 效果

    • 各模塊職責清晰,單元測試覆蓋率提升至 90%
    • 修改通知邏輯時,無需回歸庫存或支付流程

二、開閉原則(OCP)—優惠策略引擎

項目背景
促銷活動層出不窮,初期將 DiscountService 寫成多重 if-else,每次上線新活動都要改這個類,風險極高。

問題痛點

  • 修改封閉,新增促銷需頻繁改動原有代碼
  • 條件分支難以維護,代碼臃腫

OCP 應用

  1. 抽象策略接口

    public interface DiscountStrategy {BigDecimal calculate(Order order);
    }
    
  2. 各活動實現

    @Component
    public class BlackFridayStrategy implements DiscountStrategy { /*…*/ }@Component
    public class NewUserStrategy implements DiscountStrategy { /*…*/ }
    
  3. 策略注冊與調用

    @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);}
    }
    
  4. 效果

    • 新增策略只需編寫一個類并注入,無需改動 DiscountService
    • 代碼體量更易擴展,回歸風險大幅降低

三、里氏替換原則(LSP)—圖表渲染組件

項目背景
在后臺統計系統中,需要渲染不同類型的圖表(折線圖、柱狀圖、餅圖)。最初用 Chart 抽象類配合 if (type) 邏輯,后來改用繼承。

問題痛點

  • 部分子類沒有實現所有方法,導致運行時拋出 UnsupportedOperationException
  • 修改父類抽象方法會破壞部分子類行為

LSP 應用

  1. 精煉抽象

    public interface ChartRenderer {void render(DataSet data);
    }
    
  2. 具體子類全力支持契約

    public class LineChartRenderer implements ChartRenderer { /*…*/ }
    public class PieChartRenderer  implements ChartRenderer { /*…*/ }
    
  3. 渲染調用無需分支

    rendererMap.get(type).render(data);
    
  4. 效果

    • 所有子類都能安全替換接口
    • 后續新增 RadarChartRenderer 無需改動核心邏輯

四、接口隔離原則(ISP)—外部服務集成

項目背景
一套 CRM 系統需要對接多家短信、郵件、推送服務,最初定義一個 MessagingClient 接口,包含 sendSmssendEmailsendPush,導致集成方只需郵件時也要實現短信、推送方法。

問題痛點

  • 實現類方法樁多,代碼臃腫
  • 不同服務方復用率低

ISP 應用

  1. 拆分接口

    public interface SmsClient   { void sendSms(SmsMessage msg); }
    public interface EmailClient { void sendEmail(Email msg); }
    public interface PushClient  { void sendPush(PushMessage msg); }
    
  2. 各接入實現各自接口

    public class TwilioSmsClient implements SmsClient { /*…*/ }
    public class SendGridEmailClient implements EmailClient { /*…*/ }
    
  3. 按需注入

    @Service
    public class NotificationService {private final SmsClient sms;private final EmailClient email;public void notifyOrderCreated(Order o) {sms.sendSms(...);email.sendEmail(...);}
    }
    
  4. 效果

    • 避免“被迫”實現無關方法
    • 接口職責更聚焦,單元測試更簡潔

五、依賴反轉原則(DIP)—倉儲層設計

項目背景
某金融系統最初直接在業務層 LoanServicenew JdbcLoanDao(),測試時需要配合真實數據庫,耦合度高。

問題痛點

  • 測試難以模擬,業務層依賴底層實現
  • 更換存儲方式需改動業務層

DIP 應用

  1. 抽象 DAO 接口

    public interface LoanRepository {Loan findById(String id);void save(Loan loan);
    }
    
  2. 業務層依賴接口

    public class LoanService {private final LoanRepository repo;public LoanService(LoanRepository repo){ this.repo = repo; }// … 調用 repo 方法
    }
    
  3. 底層實現注入

    @Repository
    public class JdbcLoanRepository implements LoanRepository { /*…*/ }
    
  4. 效果

    • 單元測試可注入內存或 Mock 實現
    • 切換到 JPA 或其它存儲無業務層改動

案例共性與最佳實踐

  1. 先識別“變化點”,再拆分職責或抽象接口。
  2. 不要為了原則而原則,關注業務痛點與演進成本。
  3. 測試驅動設計(TDD) 有助于發現違反 SOLID 的耦合點。
  4. KISS 平衡:遵循 SOLID 的同時,也要兼顧代碼簡潔與團隊可讀性。

在這里插入圖片描述

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

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

相關文章

Vue 3 中父組件內兩個子組件相互傳參的幾種方法

方法一&#xff1a;通過父組件中轉&#xff08;Props Emits&#xff09;<!-- ParentComponent.vue --> <template><ChildA :message-from-b"messageFromB" send-to-b"handleSendToB" /><ChildB :message-from-a"messageFromA&q…

三子棋游戲設計與實現(C 語言版)

一、需求分析目標&#xff1a;實現一個簡單的人機對戰三子棋&#xff0c;支持以下功能&#xff1a;初始化空棋盤&#xff0c;清晰展示落子狀態。玩家通過坐標落子&#xff08;X 代表玩家&#xff09;&#xff0c;電腦隨機落子&#xff08;O 代表電腦&#xff09;。實時判斷勝負…

GD32 CAN1和TIMER0同時開啟問題

背景&#xff1a;今天在一個項目調試的時候發現了一些問題&#xff0c;由此貼記錄一下問題解決的過程。使用的芯片是GD32F305VE。使用到了CAN1和TIMER0。在使用這連個外設的時候發送了一些問題。單獨使用CAN1。功能正常。單獨使用TIMER0。配置為輸出模式。功能正常。但是當兩個…

劍指offer56_數組中唯一只出現一次的數字

數組中唯一只出現一次的數字在一個數組中除了一個數字只出現一次之外&#xff0c;其他數字都出現了三次。 請找出那個只出現一次的數字。 你可以假設滿足條件的數字一定存在。 思考題&#xff1a; 如果要求只使用 O(n) 的時間和額外 O(1) 的空間&#xff0c;該怎么做呢&#xf…

從語音識別到智能助手:Voice Agent 的技術進化與交互變革丨Voice Agent 學習筆記

From Research AI&#xff1a; 最近看到 Andrew Ng 的一句話讓我印象深刻&#xff1a;“While some things in AI are overhyped, voice applications seem underhyped right now.”&#xff08;盡管 AI 中有些領域被過度炒作&#xff0c;語音應用卻似乎被低估了&#xff09;。…

什么是Jaccard 相似度(Jaccard Similarity)

文章目錄? 定義&#xff1a;&#x1f4cc; 取值范圍&#xff1a;&#x1f50d; 舉例說明&#xff1a;&#x1f9e0; 應用場景&#xff1a;?? 局限性&#xff1a;&#x1f4a1; 擴展概念&#xff1a;Jaccard 相似度&#xff08;Jaccard Similarity&#xff09; 是一種用于衡量…

ragflow_多模態文檔解析與正文提取策略

多模態文檔解析與正文提取策略 RAGflow的文檔解析系統位于deepdoc/parser/目錄下,實現了對多種文檔格式的統一解析處理。該系統采用模塊化設計,針對不同文檔格式提供專門的解析器,并通過視覺識別技術增強解析能力。本文將深入探討RAGflow的文檔解析系統的設計原理、實現細節…

數據結構棧的實現(C語言)

棧的基本概念棧是一種特殊的線性存儲結構&#xff0c;是一種操作受到限制的線性表&#xff0c;特殊體現在兩個地方&#xff1a;1、元素進棧出棧的操作只能從同一端完成&#xff0c;另一端是封閉的&#xff0c;通常將數據進棧叫做入棧&#xff0c;壓棧等&#xff0c;出棧叫做彈棧…

【springboot】IDEA手動創建SpringBoot簡單工程(無插件)

大致步驟 創建Maven工程 引入依賴 提供啟動類 詳細教程 創建Maven工程 修改pom.xml文件 添加父節點 <parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>3.5.3</…

獨立開發第二周:構建、執行、規劃

一 第二周的獨立開發旅程落下帷幕。相較于第一周的適應&#xff0c;本周的核心詞是“聚焦”與“執行”。 目標非常明確&#xff1a;在產品開發上取得進展&#xff1b;在個人工作節奏上&#xff0c;將上周初步形成的框架進行實踐與固化。 同時&#xff0c;為至關重要的自媒體運營…

在YOLO-World中集成DeformConv、CBAM和Cross-Modal Attention模塊的技術報告

在YOLO-World中集成DeformConv、CBAM和Cross-Modal Attention模塊的技術報告 1. 引言 1.1 項目背景 目標檢測是計算機視覺領域的核心任務之一,而YOLO(You Only Look Once)系列算法因其出色的速度和精度平衡而廣受歡迎。YOLO-World是YOLO系列的最新發展,專注于開放詞匯目標…

從UI設計到數字孿生實戰應用:構建智慧金融的風險評估與預警平臺

hello寶子們...我們是艾斯視覺擅長ui設計、前端開發、數字孿生、大數據、三維建模、三維動畫10年經驗!希望我的分享能幫助到您!如需幫助可以評論關注私信我們一起探討!致敬感謝感恩!一、引言&#xff1a;傳統金融風控的 “滯后困境” 與數字孿生的破局之道金融風險的隱蔽性、突…

【Linux】權限相關指令

前言&#xff1a; 上兩篇文章我們講到了&#xff0c;關于Linux中的基礎指令。 【Linux】初見&#xff0c;基礎指令-CSDN博客【Linux】初見&#xff0c;基礎指令&#xff08;續&#xff09;-CSDN博客 本文我們來講Linux中關于權限中的一些指令 shell命令 Linux嚴格來說是一個操…

前端學習3--position定位(relative+absolute+sticky)

繼上一篇&#xff0c;做下拉菜單的時候&#xff0c;涉及到了position&#xff0c;這篇就來學習下~先看下position在下拉菜單中的應用&#xff1a;一、關鍵代碼回顧&#xff1a;/* 下拉菜單容器 */ .dropdown {position: relative; /* ? 關鍵父級 */ }/* 下拉內容&#xff08;默…

APP Inventor使用指南

APP Inventor使用指南一、組件介紹二、邏輯設計設計方法&#xff1a;設計實例&#xff08;參考嘉立創教程&#xff09;點擊跳轉 &#xff1a; app inventor&#xff08;點不開的話需要&#x1fa84;&#x1fa84;&#x1fa84;&#x1fa84;&#x1fa84;&#xff09; 一、組…

SpringAI實現保存聊天記錄到redis中

redis相關準備添加依賴我利用redission來實現<dependency><groupId>org.redisson</groupId><artifactId>redisson</artifactId><version>3.37.0</version> </dependency>添加配置文件spring:redis:database: 5host: 127.0.0.1…

Unity中使用EzySlice實現模型切割與UV控制完全指南

引言 在Unity中實現3D模型的動態切割是一個常見的需求&#xff0c;無論是用于游戲特效、建筑可視化還是醫療模擬。本文將全面介紹如何使用EzySlice插件實現高效的模型切割&#xff0c;并深入探討如何通過Shader Graph精確控制切割面的UV映射。 第一部分&#xff1a;EzySlice基…

【c++學習記錄】狀態模式,實現一個登陸功能

狀態模式建議為對象的所有可能狀態新建一個類&#xff0c; 然后將所有狀態的對應行為抽取到這些類中。 原始對象被稱為上下文 &#xff08;context&#xff09;&#xff0c; 它并不會自行實現所有行為&#xff0c; 而是會保存一個指向表示當前狀態的狀態對象的引用&#xff0c;…

Docker 搭建 Harbor 私有倉庫

1 部署 Harbor 注意&#xff1a;docker、docker-compose、Harbor的版本是否適配&#xff0c;這里使用的版本如下表&#xff1a; Docker版本Docker Compose版本Harbor版本v19.09.8v1.29.2v2.8.2 1.1 安裝 docker-compose # 下載 docker-compose 1.29.2 版本 curl -L "h…

C++類模板繼承部分知識及測試代碼

目錄 0.前言 1.類模板基本使用 2.類模板繼承 2.1類模板繼承過程中的模板參數 情況1&#xff1a;父類非模板&#xff0c;子類為模板 情況2&#xff1a;父類模板&#xff0c;子類為非模板 情況3&#xff1a;父類模板&#xff0c;子類為模板 3.STL中的模板類分析 3.1STL中…