工作那麼久,才知道的 SOLID 設計原則
阿新 • • 發佈:2020-07-07
## 認識 SOLID 原則
無論是軟體系統設計,還是程式碼實現,遵循有效和明確的設計原則,都利於系統軟體靈活可靠,安全快速的落地,更重要的是能靈活地應對需求,簡化系統擴充套件和維護,避免無效的加班。本文主要討論面向物件軟體開發中最流行的設計原則- SOLID,它是五個設計原則為了方便記憶而組成的首字母縮寫:
- 單一職責原則
- 開放/封閉原則
- 裡式替換原則
- 介面隔離原則
- 依賴倒置原則
## S:單一職責原則 (SRP)
#### 基本概念
單一職責原則 (SRP) 英文全稱為 Single Responsibility Principle,是最簡單,但也是最難用好的原則之一。它的定義也很簡單:對於一個類而言,應該僅有一個引起它變化的原因。其中變化的原因就表示了這個類的職責,它可能是某個特定領域的功能,可能是某個需求的解決方案。
這個原則表達的是不要讓一個類承擔過多的責任,一旦有了多個職責,那麼它就越容易因為某個職責而被更改,這樣的狀態是不穩定的,不經意的修改很有可能影響到這個類的其他功能。因此,我們需要將不同的職責封裝在不同的類中,即將不同的變化原因封裝在不同的類中,不同類之間的變化互不影響。 #### 例項說明 舉一個具體的例子,有一個用於實現編輯和列印報表的類,這樣的類存在兩個變化的原因:第一,報表的內容可以改變(編輯)。第二,報表的格式可以改變(列印)。如果有一個對於報表編輯流程的修改,而報表的編輯流程會導致公共狀態或者依賴關係的改變,使得列印功能的程式碼無法工作。所以單一職責原則認為這兩個變化的原因事實上是兩個分離的功能,它們應該分離在不同的類中。
![image.png](https://img2020.cnblogs.com/other/664672/202007/664672-20200707124635185-1489130459.png)
#### 相關設計模式 面對違背單一職責原則的程式程式碼,我們可以利用外觀模式,代理模式,橋接模式,介面卡模式,命令模式對已有設計進行重構,實現多職責的分離。
#### 小結 單一職責原則用於控制類的粒度大小,減少類中不相關功能的程式碼耦合,使得類更加的健壯;另外,單一職責原則也適用於模組之間解耦,對於模組的功能劃分有很大的指導意義。
## O:開閉原則 (OCP) #### 基本概念 開閉原則 (OCP) 英文全稱為 Open-Closed Principle,基本定義是軟體中的物件(類,模組,函式等)應該對於擴充套件是開放的,但是對於修改是封閉的。這裡的**對擴充套件開放**表示這新增新的程式碼,就可以讓程式行為擴充套件來滿足需求的變化;**對修改封閉**表示在擴充套件程式行為時不要修改已有的程式碼,進而避免影響原有的功能。
要實現不改程式碼的情況下,仍要去改變系統行為的關鍵就是抽象和多型,通過介面或者抽象類定義系統的抽象層,再通過具體類來進行擴充套件。這樣一來,無須對抽象層進行任何改動,只需要增加新的具體類來實現新的業務功能即可,達到開閉原則的要求。 #### 例項說明 同樣,舉個例子來更深刻地理解開閉原則:有一個用於圖表顯示的 Display 類,它能繪製各種型別的圖表,比如餅狀圖,柱狀圖等;而需要繪製特定圖表時,都強依賴了對應型別的圖表,Display 類的內部實現如下: ```java public void display(String type) { if (type.equals("pie")) { PieChart chart = new PieChart(); chart.display(); } else if (type.equals("bar")) { BarChart chart = new BarChart(); chart.display(); } } ``` 基於上述的程式碼,如果需要新增一個圖表,比如折線圖 LineChart ,就要修改 Display 類的 display() 方法,增加新增的判斷邏輯,很顯然這樣的做法違反開閉原則。而讓類的實現符合開閉原則的方式就是引入抽象圖表類 AbstractChart,作為其他圖表的基類,讓 Display 依賴這個抽象圖表類 AbstractChart,然後通過 Display 決定使用哪種具體的圖表類,實現程式碼變成了這樣: ```java private Abstractchart chart; public void display() { chart.display(); } ``` 現在我們需要新增折線圖顯示,在客戶端向 Display 中注入一個 LineChart 物件即可,無須修改現有類庫的原始碼。
![image.png](https://img2020.cnblogs.com/other/664672/202007/664672-20200707124635482-1135848815.png)
#### 相關設計模式 面對違背開閉原則的程式程式碼,可以用到的設計模式有很多,比如工廠模式,觀察者模式,模板方法模式,策略模式,組合模式,使用相關設計模式的關鍵點就是識別出最有可能變化和擴充套件的部分,然後構造抽象來隔離這些變化。
#### 小結 有了開閉原則,面向需求的變化就能進行快速的調整實現功能,這大大提高系統的靈活性,可重用性和可維護性,但會增加一定的複雜性。
## L: 裡式替換原則 (LSP) #### 基本概念 裡式替換原則 (LSP) 英文全稱為 Liskov Substitution Principle,基本定義為:在不影響程式正確性的基礎上,所有使用基類的地方都能使用其子類的物件來替換。這裡提到的基類和子類說的就是具有繼承關係的兩類物件,當我們傳遞一個子型別物件時,需要保證程式不會改變任何原基類的行為和狀態,程式能正常運作。
#### 例項說明 為了能理解裡式替換原則,這裡舉一個經典的違反裡式替換原則的例子:正方形/長方形問題。
![image.png](https://img2020.cnblogs.com/other/664672/202007/664672-20200707124635782-1550221092.png)
上圖為正方形/長方形問題的類層次結構,Square 類繼承了 Rectangle 類,但是 Rectangle 類的寬高可以分別修改,但是 Suqare 類的寬高則必須一同修改。如果 User 類操作 Rectangle 類時,但實際物件是 Suqare 型別時,就會造成程式的出錯,如下方程式碼: ```java Rectangle r = ...; // 返回具體型別物件 r.setWidth(5); r.setHeight(2); assert(r.area() == 10); ``` 當返回具體型別物件為 Suqare 型別,面積為 10 的斷言就是失敗,這樣明顯是不符合裡式替換原則的。 #### 小結 要讓程式程式碼符合裡式替換原則,需要保證子類繼承父類時,除新增新的方法完成新增功能外,儘量不要重寫父類的方法,換句話就是子類可以擴充套件父類的功能,但不能改變父類原有的功能。
另一方面,裡式替換原則也是對開閉原則的補充,不僅適用於繼承關係,還適用於實現關係的設計,常提到的 IS-A 關係是針對行為方式來說的,如果兩個類的行為方式是不相容,那麼就不應該使用繼承,更好的方式是提取公共部分的方法來代替繼承。
## I:介面隔離原則 (ISP) #### 基本概念 介面隔離原則 (ISP) 英文全稱為 Interface Segregation Principle,基本定義:客戶端不應該依賴那些它不需要的介面。客戶端應該只依賴它實際使用的方法,因為如果一個介面具備了若干個方法,那就意味著它的實現類都要實現所有介面方法,從程式碼結構上就十分臃腫。
#### 例項說明
![image.png](https://img2020.cnblogs.com/other/664672/202007/664672-20200707124635956-1589697674.png)
現在我們看下一個違反介面隔離原則的例子,從上面類結構圖中,有多個使用者需要操作 Operation 類。如果 User1 只需要使用 operation1 方法,User2 只需要使用 operation2 方法,User3 只需要使用 operation3 方法,那麼很明顯對於 User1 來說,不應該看到 operation2 和 operation3 這兩個方法,要減少對自己不關心的方法的依賴,防止 Operation 類中 operation2 和 operation3 方法的修改,影響到 User1 的功能。這個問題可以通過將不同的操作隔離成獨立的介面來解決,具體如下圖所示。
![image.png](https://img2020.cnblogs.com/other/664672/202007/664672-20200707124636117-1368475012.png)
基於介面隔離原則,我們需要做的就是減少定義大而全的介面,類所要實現的介面應該分解成多個介面,然後根據所需要的功能去實現,並且在使用到介面方法的地方,用對應的介面型別去宣告,這樣可以解除呼叫方與物件非相關方法的依賴關係。總結一下,介面隔離原則主要功能就是控制介面的粒度大小,防止暴露給客戶端無相關的程式碼和方法,保證了介面的高內聚,降低與客戶端的耦合。 ## D:依賴倒置原則 (DIP) #### 基本概念 依賴倒置原則 (DIP) 英文全稱 Dependency Inversion Principle, DIP),基本定義是: - 高層模組不應該依賴低層模組,應該共同依賴抽象; - 抽象不應該依賴細節,細節應該依賴抽象。 這裡的抽象就是介面和抽象類,而細節就是實現介面或繼承抽象類而產生的類。
#### 例項說明 如何理解“高層模組不應該依賴低層模組,應該共同依賴抽象”呢?如果高層模組依賴於低層模組,那麼低層模組的改動很有可能影響到高層模組,從而導致高層模組被迫改動,這樣一來讓高層模組的重用變得非常困難。 ![file](https://img2020.cnblogs.com/other/664672/202007/664672-20200707124636448-833217590.jpg)而最佳的做法就如上圖一樣,在高層模組構建一個穩定的抽象層,並且只依賴這個抽象層;而由底層模組完成抽象層的實現細節。這樣一來,高層類都通過該抽象介面使用下一層,移除了高層對底層實現細節的依賴。 #### 相關設計模式 關於依賴倒置原則,可以用到的設計模式有工廠模式,模板方法模式,策略模式。 #### 小結 依賴倒置原則可以減少類間的耦合性,提高系統的穩定性,降低並行開發引起的風險,提高程式碼的可讀性和可維護性。同時依賴倒置原則也是框架設計的核心原則,善於建立可重用的框架和富有擴充套件性的程式碼,比如 Tomcat 容器的 Servlet 規範實現,Spring Ioc 容器實現。
## 結語 到這裡,SOLID 設計原則就全部介紹完了,本文的主要目的還是對這六項原則系統地整理和總結,在後續的程式設計開發過程中能有意識地識別出設計原則和模式。如果大家對設計原則有更多想法和理解,歡迎留言,大家共同探討。
![](https://img2020.cnblogs.com/other/664672/202007/664672-20200707124636611-1656108069.jpg) > 本文由部落格一文多發平臺 [OpenWrite](https://openwrite.cn?from=article_bottom)
這個原則表達的是不要讓一個類承擔過多的責任,一旦有了多個職責,那麼它就越容易因為某個職責而被更改,這樣的狀態是不穩定的,不經意的修改很有可能影響到這個類的其他功能。因此,我們需要將不同的職責封裝在不同的類中,即將不同的變化原因封裝在不同的類中,不同類之間的變化互不影響。 #### 例項說明 舉一個具體的例子,有一個用於實現編輯和列印報表的類,這樣的類存在兩個變化的原因:第一,報表的內容可以改變(編輯)。第二,報表的格式可以改變(列印)。如果有一個對於報表編輯流程的修改,而報表的編輯流程會導致公共狀態或者依賴關係的改變,使得列印功能的程式碼無法工作。所以單一職責原則認為這兩個變化的原因事實上是兩個分離的功能,它們應該分離在不同的類中。
![image.png](https://img2020.cnblogs.com/other/664672/202007/664672-20200707124635185-1489130459.png)
#### 相關設計模式 面對違背單一職責原則的程式程式碼,我們可以利用外觀模式,代理模式,橋接模式,介面卡模式,命令模式對已有設計進行重構,實現多職責的分離。
#### 小結 單一職責原則用於控制類的粒度大小,減少類中不相關功能的程式碼耦合,使得類更加的健壯;另外,單一職責原則也適用於模組之間解耦,對於模組的功能劃分有很大的指導意義。
## O:開閉原則 (OCP) #### 基本概念 開閉原則 (OCP) 英文全稱為 Open-Closed Principle,基本定義是軟體中的物件(類,模組,函式等)應該對於擴充套件是開放的,但是對於修改是封閉的。這裡的**對擴充套件開放**表示這新增新的程式碼,就可以讓程式行為擴充套件來滿足需求的變化;**對修改封閉**表示在擴充套件程式行為時不要修改已有的程式碼,進而避免影響原有的功能。
要實現不改程式碼的情況下,仍要去改變系統行為的關鍵就是抽象和多型,通過介面或者抽象類定義系統的抽象層,再通過具體類來進行擴充套件。這樣一來,無須對抽象層進行任何改動,只需要增加新的具體類來實現新的業務功能即可,達到開閉原則的要求。 #### 例項說明 同樣,舉個例子來更深刻地理解開閉原則:有一個用於圖表顯示的 Display 類,它能繪製各種型別的圖表,比如餅狀圖,柱狀圖等;而需要繪製特定圖表時,都強依賴了對應型別的圖表,Display 類的內部實現如下: ```java public void display(String type) { if (type.equals("pie")) { PieChart chart = new PieChart(); chart.display(); } else if (type.equals("bar")) { BarChart chart = new BarChart(); chart.display(); } } ``` 基於上述的程式碼,如果需要新增一個圖表,比如折線圖 LineChart ,就要修改 Display 類的 display() 方法,增加新增的判斷邏輯,很顯然這樣的做法違反開閉原則。而讓類的實現符合開閉原則的方式就是引入抽象圖表類 AbstractChart,作為其他圖表的基類,讓 Display 依賴這個抽象圖表類 AbstractChart,然後通過 Display 決定使用哪種具體的圖表類,實現程式碼變成了這樣: ```java private Abstractchart chart; public void display() { chart.display(); } ``` 現在我們需要新增折線圖顯示,在客戶端向 Display 中注入一個 LineChart 物件即可,無須修改現有類庫的原始碼。
![image.png](https://img2020.cnblogs.com/other/664672/202007/664672-20200707124635482-1135848815.png)
#### 相關設計模式 面對違背開閉原則的程式程式碼,可以用到的設計模式有很多,比如工廠模式,觀察者模式,模板方法模式,策略模式,組合模式,使用相關設計模式的關鍵點就是識別出最有可能變化和擴充套件的部分,然後構造抽象來隔離這些變化。
#### 小結 有了開閉原則,面向需求的變化就能進行快速的調整實現功能,這大大提高系統的靈活性,可重用性和可維護性,但會增加一定的複雜性。
## L: 裡式替換原則 (LSP) #### 基本概念 裡式替換原則 (LSP) 英文全稱為 Liskov Substitution Principle,基本定義為:在不影響程式正確性的基礎上,所有使用基類的地方都能使用其子類的物件來替換。這裡提到的基類和子類說的就是具有繼承關係的兩類物件,當我們傳遞一個子型別物件時,需要保證程式不會改變任何原基類的行為和狀態,程式能正常運作。
#### 例項說明 為了能理解裡式替換原則,這裡舉一個經典的違反裡式替換原則的例子:正方形/長方形問題。
![image.png](https://img2020.cnblogs.com/other/664672/202007/664672-20200707124635782-1550221092.png)
上圖為正方形/長方形問題的類層次結構,Square 類繼承了 Rectangle 類,但是 Rectangle 類的寬高可以分別修改,但是 Suqare 類的寬高則必須一同修改。如果 User 類操作 Rectangle 類時,但實際物件是 Suqare 型別時,就會造成程式的出錯,如下方程式碼: ```java Rectangle r = ...; // 返回具體型別物件 r.setWidth(5); r.setHeight(2); assert(r.area() == 10); ``` 當返回具體型別物件為 Suqare 型別,面積為 10 的斷言就是失敗,這樣明顯是不符合裡式替換原則的。 #### 小結 要讓程式程式碼符合裡式替換原則,需要保證子類繼承父類時,除新增新的方法完成新增功能外,儘量不要重寫父類的方法,換句話就是子類可以擴充套件父類的功能,但不能改變父類原有的功能。
另一方面,裡式替換原則也是對開閉原則的補充,不僅適用於繼承關係,還適用於實現關係的設計,常提到的 IS-A 關係是針對行為方式來說的,如果兩個類的行為方式是不相容,那麼就不應該使用繼承,更好的方式是提取公共部分的方法來代替繼承。
## I:介面隔離原則 (ISP) #### 基本概念 介面隔離原則 (ISP) 英文全稱為 Interface Segregation Principle,基本定義:客戶端不應該依賴那些它不需要的介面。客戶端應該只依賴它實際使用的方法,因為如果一個介面具備了若干個方法,那就意味著它的實現類都要實現所有介面方法,從程式碼結構上就十分臃腫。
#### 例項說明
![image.png](https://img2020.cnblogs.com/other/664672/202007/664672-20200707124635956-1589697674.png)
現在我們看下一個違反介面隔離原則的例子,從上面類結構圖中,有多個使用者需要操作 Operation 類。如果 User1 只需要使用 operation1 方法,User2 只需要使用 operation2 方法,User3 只需要使用 operation3 方法,那麼很明顯對於 User1 來說,不應該看到 operation2 和 operation3 這兩個方法,要減少對自己不關心的方法的依賴,防止 Operation 類中 operation2 和 operation3 方法的修改,影響到 User1 的功能。這個問題可以通過將不同的操作隔離成獨立的介面來解決,具體如下圖所示。
![image.png](https://img2020.cnblogs.com/other/664672/202007/664672-20200707124636117-1368475012.png)
基於介面隔離原則,我們需要做的就是減少定義大而全的介面,類所要實現的介面應該分解成多個介面,然後根據所需要的功能去實現,並且在使用到介面方法的地方,用對應的介面型別去宣告,這樣可以解除呼叫方與物件非相關方法的依賴關係。總結一下,介面隔離原則主要功能就是控制介面的粒度大小,防止暴露給客戶端無相關的程式碼和方法,保證了介面的高內聚,降低與客戶端的耦合。 ## D:依賴倒置原則 (DIP) #### 基本概念 依賴倒置原則 (DIP) 英文全稱 Dependency Inversion Principle, DIP),基本定義是: - 高層模組不應該依賴低層模組,應該共同依賴抽象; - 抽象不應該依賴細節,細節應該依賴抽象。 這裡的抽象就是介面和抽象類,而細節就是實現介面或繼承抽象類而產生的類。
#### 例項說明 如何理解“高層模組不應該依賴低層模組,應該共同依賴抽象”呢?如果高層模組依賴於低層模組,那麼低層模組的改動很有可能影響到高層模組,從而導致高層模組被迫改動,這樣一來讓高層模組的重用變得非常困難。 ![file](https://img2020.cnblogs.com/other/664672/202007/664672-20200707124636448-833217590.jpg)而最佳的做法就如上圖一樣,在高層模組構建一個穩定的抽象層,並且只依賴這個抽象層;而由底層模組完成抽象層的實現細節。這樣一來,高層類都通過該抽象介面使用下一層,移除了高層對底層實現細節的依賴。 #### 相關設計模式 關於依賴倒置原則,可以用到的設計模式有工廠模式,模板方法模式,策略模式。 #### 小結 依賴倒置原則可以減少類間的耦合性,提高系統的穩定性,降低並行開發引起的風險,提高程式碼的可讀性和可維護性。同時依賴倒置原則也是框架設計的核心原則,善於建立可重用的框架和富有擴充套件性的程式碼,比如 Tomcat 容器的 Servlet 規範實現,Spring Ioc 容器實現。
## 結語 到這裡,SOLID 設計原則就全部介紹完了,本文的主要目的還是對這六項原則系統地整理和總結,在後續的程式設計開發過程中能有意識地識別出設計原則和模式。如果大家對設計原則有更多想法和理解,歡迎留言,大家共同探討。
![](https://img2020.cnblogs.com/other/664672/202007/664672-20200707124636611-1656108069.jpg) > 本文由部落格一文多發平臺 [OpenWrite](https://openwrite.cn?from=article_bottom)