1. 程式人生 > >SOLID架構設計原則

SOLID架構設計原則

最近通讀了《架構整潔之道》,受益匪淺,遂摘選出設計原則部分,與大家分享,希望大家能從中獲益。 以下為書中第3部分 設計原則的原文。 --- ### 設計原則概述 通常來說,要想構建—個好的軟體系統,應該從寫整潔的程式碼開始做起。畢竟,如果建築所使用的磚頭質量不佳,那麼架構所能起到的作用也會很有限。反之亦然,如果建築的架構設計不佳,那麼其所用的磚頭質量再好也沒有用。這就是SOLID設計原則所要解決的問題。 SOLID原則的主要作用就是告訴我們如何將資料和函式組織成為類,以及如何將這些類連結起來成為程式。請注意,這裡雖然用到了“類”這個詞,但是並不意味著我們將要討論的這些設計原則僅僅適用於面向物件程式設計。這裡的類僅僅代表了一種資料和函式的分組,每個軟體系統都會有自己的分類系統,不管它們各自是不是將其稱為“類”,事實上都是SOLID原則的適用領域。 一般情況下,我們為軟體構建中層結構的主要目標如下: + 使軟體可容忍被改動 + 使軟體更容易被理解 + 構建可在多個軟體系統中複用的元件 我們在這裡之所以會使用“中層”這個詞,是因為這些設計原則主要適用於那些進行模組級程式設計的程式設計師。SO凵D原則應該直接緊貼於具體的程式碼邏輯之上,這些原則是用來幫助我們定義軟體架構中的元件和模組的。 當然了,正如用好磚也會蓋歪樓一樣,採用設計良好的中層元件並不能保證系統的整體架構運作良好。正因為如此,我們在講完SOLID原則之後,還會再繼續針對元件的設計原則進行更進一步的討論,將其推進到高階軟體架構部分。 SOLID原則的歷史已經很悠久了,早在20世紀80年代末期,我在 USENET新聞組(該新聞組在當時就相當於今天的 Facebook)上和其他人辯論軟體設計理念的時候,該設計原則就已經開始逐漸成型了。隨著時間的推移,其中有一些原則得到了修改,有一些則被拋棄了,還有一些被合併了,另外也增加了一些。它們的最終形態是在2000年左右形成的,只不過當時採用的是另外一個展現順序。 2004年前後, Michael feathers的一封電子郵件提醒我:如果重新排列這些設計原則,那麼它們的首字母可以排列成SOLID——這就是SOLID原則誕生的故事。 在這一部分中,我們會逐章地詳細討論每個設計原則,下面先來做一個簡單摘要。 **SRP:單一職責原則。** 該設計原則是基於康威定律( Conway‘s Law)的一個推論——軟體系統的最佳結構高度依賴於開發這個系統的組織的內部結構。這樣,每個軟體模組都有且只有一個需要被改變的理由。 **OCP:開閉原則。** 該設計原則是由 Bertrand Meyer在20世紀80年代大力推廣的,其核心要素是:如果軟體系統想要更容易被改變,那麼其設計就必須允許新增程式碼來修改系統行為,而非只能靠修改原來的程式碼。 **LSP:里氏替換原則。** 該設計原則是 Barbara liskov在1988年提出的著名的子型別定義。簡單來說,這項原則的意思是如果想用可替換的元件來構建軟體系統,那麼這些元件就必須遵守同一個約定,以便讓這些元件可以相互替換。 **ISP:介面隔離原則。** 這項設計原則主要告誡軟體設計師應該在設計中避免不必要的依賴。 **DIP:依賴反轉原則。** 該設計原則指出高層策略性的程式碼不應該依賴實現底層細節的程式碼,恰恰相反,那些實現底層細節的程式碼應該依賴高層策略性的程式碼。 這些年來,這些設計原則在很多不同的出版物中都有過詳細描述。在接下來的章節中,我們將會主要關注這些原則在軟體架構上的意義,而不再重複其細節資訊。如果你對這些原則並不是特別瞭解,那麼我建議你先通過腳註中的文件熟悉一下它們,否則接下來的章節可能有點難以理解。 ### SRP:單一職責原則 SRP是SOLID五大設計原則中最容易被誤解的一。也許是名字的原因,很多程式設計師根據SRP這個名字想當然地認為這個原則就是指:每個模組都應該只做一件事。 沒錯,後者的確也是一個設計原則,即確保一個函式只完成一個功能。我們在將大型函式重構成小函式時經常會用到這個原則,但這只是一個面向底層實現細節的設計原則,並不是SRP的全部。 在歷史上,我們曾經這樣描述SRP這一設計原則: 任何一個軟體模組都應該有且僅有一個被修改的原因。 在現實環境中,軟體系統為了滿足使用者和所有者的要求,必然要經常做出這樣那樣的修改。而該系統的使用者或者所有者就是該設計原則中所指的“被修改的原因”。所以,我們也可以這樣描述SRP: 任何一個軟體模組都應該只對一個使用者(User)或系統利益相關者( Stakeholder)負責。 不過,這裡的“使用者”和“系統利益相關者”在用詞上也並不完全準確,它們很有可能指的是一個或多個使用者和利益相關者,只要這些人希望對系統進行的變更是相似的,就可以歸為一類——一個或多有共同需求的人。在這裡,我們將其稱為行為者( actor)。 所以,對於SRP的最終描述就變成了: 任何一個軟體模組都應該只對某一類行為者負責。 那麼,上文中提剄的“軟體模組”究竟又是在指什麼呢?大部分情況下,其最簡單的定義就是指一個原始碼檔案。然而,有些程式語言和程式設計環境並不是用原始碼檔案來儲存程式的。在這些情況下,“軟體模組”指的就是一組緊密相關的函式和資料結構。 在這裡,“相關”這個詞實際上就隱含了SRP這一原則。程式碼與資料就是靠著與某一類行為者的相關性被組合在一起的。 或許,理解這個設計原則最好的辦法就是讓大家來看一些反面案例。 **反面案例1:重複的假象。** 這是我最喜歡舉的一個例子:某個工資管理程式中的 Employee類有三個函式 calculate Pay()、reportHours()和save()。 ![](https://img2020.cnblogs.com/blog/1387660/202103/1387660-20210301223626247-621915691.jpg) 如你所見,這個類的三個函式分別對應的是三類非常不同的行為者,違反了SRP設計原則。 calculatePay()函式是由財務部門制定的,他們負責向CFO彙報。 reportHours()函式是由人力資源部門制定並使用的,他們負責向COO彙報。 save()函式是由DBA制定的,他們負責向CTO彙報。 這三個函式被放在同一個原始碼檔案,即同一個Employee類中,程式設計師這樣做實際上就等於使三類行為者的行為耦合在了一起,這有可能會導致CFO團隊的命令影響到COO團隊所依賴的功能。 例如, calculatePay()函式和 reportHours()函式使用同樣的邏輯來計算正常工作時數。程式設計師為了避免重複編碼,通常會將該演算法單獨實現為個名為 regularHours()的函式(見下圖)。 ![](https://img2020.cnblogs.com/blog/1387660/202103/1387660-20210301223338136-1129682094.jpg) 接下來,假設CFO團隊需要修改正常工作時數的計算方法,而COO帶領的HR團隊不需要這個修改,因為他們對資料的用法是不同的。 這時候,負責這項修改的程式設計師會注意到calculate Pay()函式呼叫了 regularHours()函式,但可能不會注意到該函式會同時被reportHours()呼叫。 於是,該程式設計師就這樣按照要求進行了修改,同時CFO團隊的成員驗證了新演算法工作正常。這項修改最終被成功部署上線了。 但是,COO團隊顯然完全不知道這些事情的發生,HR仍然在使用 reportHours()產生的報表,隨後就會發現他們的資料出錯了!最終這個問題讓COO十分憤怒,因為這些錯誤的資料給公司造成了幾百萬美元的損失。 與此類似的事情我們肯定多多少少都經歷過。這類問題發生的根源就是因為我們將不同行為者所依賴的程式碼強湊到了一起。對此,SRP強調這類程式碼一定要被分開。 **反面案例2:程式碼合併** 一個擁有很多函式的原始碼檔案必然會經歷很多次程式碼合併,該檔案中的這些函式分別服務不同行為者的情況就更常見了。 例如,CTO團隊的DBA決定要對 Employee資料庫表結構進行簡單修改。與此同時,COO團隊的HR需要修改工作時數報表的格式。 這樣一來,就很可能出現兩個來自不同團隊的程式設計師分別對 Employee類進行修改的情況。不出意外的話,他們各自的修改一定會互相沖突,這就必須要進行程式碼合併。 在這個例子中,這次程式碼合併不僅有可能讓CTO和COO要求的功能出錯,甚至連CFO原本正常的功能也可能受到影響。 事實上,這樣的案例還有很多,我們就不一一列舉了。它們的一個共同點是,多人為了不同的目的修改了同一份原始碼,這很容易造成問題的產生。 而避免這種問題產生的方法就是將服務不同行為者的程式碼進行切分。 **解決方案** 我們有很多不同的方法可以用來解決上面的問題每一種方法都需要將相關的函式劃分成不同的類。 其中,最簡單直接的辦法是將資料與函式分離,設計三個類共同使用一個不包括函式的、十分簡單的EmployeeData類(見下圖),每個類只包含與之相關的函式程式碼,互相不可見,這樣就不存在互相依賴的情況了。 ![](https://img2020.cnblogs.com/blog/1387660/202103/1387660-20210301223355858-510775979.jpg) 這種解決方案的壞處在於:程式設計師現在需要在程式裡處理三個類。另一種解決辦法是使用 Facade設計模式(見下圖)。 ![](https://img2020.cnblogs.com/blog/1387660/202103/1387660-20210301223410152-238562954.jpg) 這樣一來, Employee Facade類所需要的程式碼量就很少了,它僅僅包含了初始化和呼叫三個具體實現類的函式。 當然,也有些程式設計師更傾向於把最重要的業務邏輯與資料放在一起,那麼我們也可以選擇將最重要的函式保留在 Employee類中,同時用這個類來呼叫其他沒那麼重要的函式(見下圖)。 ![](https://img2020.cnblogs.com/blog/1387660/202103/1387660-20210301223428542-443259132.jpg) 讀者也許會反對上面這些解決方案,因為看上去這裡的每個類中都只有一個函式。事實上並非如此,因為無論是計算工資、生成報表還是儲存資料都是一個很複雜的過程,每個類都可能包含了許多私有函式。 總而言之,上面的每一個類都分別容納了一組作用於相同作用域的函式,而在該作用域之外,它們各自的私有函式是互相不可見的。 **本章小結** 單一職責原則主要討論的是函式和類之間的關係——但是它在兩個討論層面上會以不同的形式出現。在元件層面,我們可以將其稱為共同閉包原則( Common Closure Principle),在軟體架構層面,它則是用於奠定架構邊界的變更軸心( Axis of Change)。我們在接下來的章節中會深入學習這些原則。 ### OCP:開閉原則 開閉原則(OCP)是 Bertrand Meyer在1988年提出的,該設計原則認為: 設計良好的計算機軟體應該易於擴充套件,同時抗拒修改。 換句話說,一個設計良好的計算機系統應該在不需要修改的前提下就可以輕易被擴充套件。 其實這也是我們研究軟體架構的根本目的。如果對原始需求的小小延伸就需要對原有的軟體系統進行大幅修改,那麼這個系統的架構設計顯然是失敗的。 儘管大部分軟體設計師都已經認可了OCP是設計類與模組時的重要原則,但是在軟體架構層面,這項原則的意義則更為重大。 下面,讓我們用一個思想實驗來做一些說明。 **思想實驗** 假設我們現在要設計一個在Web頁面上展示財務資料的系統,頁面上的資料要可以滾動顯示,其中負值應顯示為紅色。 接下來,該系統的所有者又要求同樣的資料需要形成一個報表,該報表要能用黑白印表機列印,並且其報表格式要得到合理分頁,每頁都要包含頁頭、頁尾及欄目名。同時,負值應該以括號表示。 顯然,我們需要增加一些程式碼來完成這個要求。但在這裡我們更關注的問題是,滿足新的要求需要更改多少舊程式碼。 一個好的軟體架構設計師會努力將舊程式碼的修改需求量降至最小,甚至為0。 但該如何實現這一點呢?我們可以先將滿足不同需求的程式碼分組(即SRP),然後再來調整這些分組之間的依賴關係(即DIP) 利用SRP,我們可以按下圖中所展示的方式來處理資料流。即先用一段分析程式處理原始的財務資料,以形成報表的資料結構,最後再用兩個不同的報表生成器來產生報表。 ![](https://img2020.cnblogs.com/blog/1387660/202103/1387660-20210301223445085-1401836127.jpg) 這裡的核心就是將應用生成報表的過程拆成兩個不同的操作。即先計算出報表資料,再生成具體的展示報表(分別以網頁及紙質的形式展示)。 接下來,我們就該修改其原始碼之間的依賴關係了。這樣做的目的是保證其中一個操作被修改之後不會影響到另外一個操作。同時,我們所構建的新的組織形式應該保證該程式後續在行為上的擴充套件都無須修改現有程式碼。 在具體實現上,我們會將整個程式程序劃分成一系列的類,然後再將這些類分割成不同的元件。下面,我們用下圖中的那些雙線框來具體描述一下整個實現。在這個圖中,左上角的元件是Controller,右上角是 Interactor,右下角是Database,左下角則有四個元件分別用於代表不同的 Presente和VieW。 在圖中,用“I”標記的類代表接