修煉內功:萬字長文詳解面向對象的設計原則

{"type":"doc","content":[{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"前言","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在我們追逐互聯網高併發技術時,應該提前打好基礎。面向對象設計原則是成爲架構的必由之路,通讀此文,反覆咀嚼,定會受益無窮。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"通常來說,要想構建一個好的軟件系統,應該從寫整潔的代碼開始做起。畢竟,如果建築所使用的磚頭質量不佳,那麼架構所能起到的作用也會很有限。反之亦然,如果建築的架構設計不佳,那麼其所用的磚頭質量再好也沒有用。這就是SOLID設計原則所要解決的問題。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SOLID原則的主要作用就是告訴我們如何將數據和函數組織成爲類,以及如何將這些類鏈接起來成爲程序。請注意,這裏雖然用到了“類”這個詞,但是並不意味着我們將要討論的這些設計原則僅僅適用於面向對象編程。這裏的類僅僅代表了一種數據和函數的分組,每個軟件系統都會有自己的分類系統,不管它們各自是不是將其稱爲“類”,事實上都是SOLID原則的適用領域。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一般情況下,我們爲軟件構建中層結構的主要目標如下:","attrs":{}}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"使軟件可容忍被改動。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"使軟件更容易被理解。","attrs":{}}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"構建可在多個軟件系統中複用的組件。","attrs":{}}]}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們在這裏之所以會使用“中層”這個詞,是因爲這些設計原則主要適用於那些進行模塊級編程的程序員。SOLID原則應該直接緊貼於具體的代碼邏輯之上,這些原則是用來幫助我們定義軟件架構中的組件和模塊的。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當然了,正如用好磚也不會蓋歪樓一樣,採用設計良好的中層組件並不能保證系統的整體架構運作良好。正因爲如此,我們在講完SOLID原則之後,還會再繼續針對組件的設計原則進行更進一步的討論,將其推進到高級軟件架構部分。","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/de/dec323ca12f9981c58c49dd16e8c2ea2.jpeg","alt":"修煉內功:萬字長文詳解面向對象的設計原則","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在這一部分中,我們會逐章地詳細討論每個設計原則,下面先來做一個簡單摘要。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"SRP:單一職責原則。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"該設計原則是基於康威定律的一個推論——一個軟件系統的最佳結構高度依賴於開發這個系統的組織的內部結構。這樣,每個軟件模塊都有且只有一個需要被改變的理由。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"OCP:開閉原則。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"該設計原則是由Bertrand Meyer在20世紀80年代大力推廣的,其核心要素是:如果軟件系統想要更容易被改變,那麼其設計就必須允許新增代碼來修改系統行爲,而非只能靠修改原來的代碼。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"LSP:里氏替換原則。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"該設計原則是Barbara Liskov在1988年提出的一個著名的子類型定義。簡單來說,這項原則的意思是如果想用可替換的組件來構建軟件系統,那麼這些組件就必須遵守同一個約定,以便讓這些組件可以相互替換。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"ISP:接口隔離原則。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這項設計原則主要告誡軟件設計師應該在設計中避免不必要的依賴。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"DIP:依賴反轉原則。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"該設計原則指出高層策略性的代碼不應該依賴實現底層細節的代碼,恰恰相反,那些實現底層細節的代碼應該依賴高層策略性的代碼。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong","attrs":{}}],"text":"SRP:單一職責原則","attrs":{}}]},{"type":"horizontalrule","attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SRP是SOLID五大設計原則中最容易被誤解的一個。也許是名字的原因,很多程序員根據SRP這個名字想當然地認爲這個原則就是指:每個模塊都應該只做一件事。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"沒錯,後者的確也是一個設計原則,即確保一個函數只完成一個功能。我們在將大型函數重構成小函數時經常會用到這個原則,但這只是一個面向底層實現細節的設計原則,並不是SRP的全部。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在歷史上,我們曾經這樣描述SRP這一設計原則:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"任何一個軟件模塊都應該有且僅有一個被修改的原因。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在現實環境中,軟件系統爲了滿足用戶和所有者的要求,必然要經常做出這樣那樣的修改。而該系統的用戶或者所有者就是該設計原則中所指的“被修改的原因”。所以,我們也可以這樣描述SRP:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"任何一個軟件模塊都應該只對一個用戶(User)或系統利益相關者(Stakeholder)負責。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"不過,這裏的“用戶”和“系統利益相關者”在用詞上也並不完全準確,它們很有可能指的是一個或多個用戶和利益相關者,只要這些人希望對系統進行的變更是相似的,就可以歸爲一類——一個或多個有共同需求的人。在這裏,我們將其稱爲行爲者(actor)。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"所以,對於SRP的最終描述就變成了:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"任何一個軟件模塊都應該只對某一類行爲者負責。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"那麼,上文中提到的“軟件模塊”究竟又是在指什麼呢?大部分情況下,其最簡單的定義就是指一個源代碼文件。然而,有些編程語言和編程環境並不是用源代碼文件來存儲程序的。在這些情況下,“軟件模塊”指的就是一組緊密相關的函數和數據結構。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在這裏,“相關”這個詞實際上就隱含了SRP這一原則。代碼與數據就是靠着與某一類行爲者的相關性被組合在一起的。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"或許,理解這個設計原則最好的辦法就是讓大家來看一些反面案例。反面案例1:重複的假象","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這是我最喜歡舉的一個例子:某個工資管理程序中的Employee類有三個函數calculatePay()、reportHours()和save()。","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/3c/3c8c00b7672dae2f9d749a821f675acd.jpeg","alt":"修煉內功:萬字長文詳解面向對象的設計原則","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如你所見,這個類的三個函數分別對應的是三類非常不同的行爲者,違反了SRP設計原則。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"calculatePay()函數是由財務部門制定的,他們負責向CFO彙報。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"reportHours()函數是由人力資源部門制定並使用的,他們負責向COO彙報。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"save()函數是由DBA制定的,他們負責向CTO彙報。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這三個函數被放在同一個源代碼文件,即同一個Employee類中,程序員這樣做實際上就等於使三類行爲者的行爲耦合在了一起,這有可能會導致CFO團隊的命令影響到COO團隊所依賴的功能。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"例如,calculatePay()函數和reportHours()函數使用同樣的邏輯來計算正常工作時數。程序員爲了避免重複編碼,通常會將該算法單獨實現爲一個名爲regularHours()的函數。","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/cd/cddb504269876edfb667e018e590e83a.jpeg","alt":"修煉內功:萬字長文詳解面向對象的設計原則","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"接下來,假設CFO團隊需要修改正常工作時數的計算方法,而COO帶領的HR團隊不需要這個修改,因爲他們對數據的用法是不同的。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這時候,負責這項修改的程序員會注意到 calculatePay()函數調用了regularHours()函數,但可能不會注意到該函數會同時被reportHours()調用。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"於是,該程序員就這樣按照要求進行了修改,同時CFO團隊的成員驗證了新算法工作正常。這項修改最終被成功部署上線了。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"但是,COO 團隊顯然完全不知道這些事情的發生,HR 仍然在使用reportHours()產生的報表,隨後就會發現他們的數據出錯了!最終這個問題讓COO十分憤怒,因爲這些錯誤的數據給公司造成了幾百萬美元的損失。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"與此類似的事情我們肯定多多少少都經歷過。這類問題發生的根源就是因爲我們將不同行爲者所依賴的代碼強湊到了一起。對此,SRP強調這類代碼一定要被分開。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"反面案例2:代碼合併","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一個擁有很多函數的源代碼文件必然會經歷很多次代碼合併,該文件中的這些函數分別服務不同行爲者的情況就更常見了。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"例如,CTO團隊的DBA決定要對Employee數據庫表結構進行簡單修改。與此同時,COO團隊的HR需要修改工作時數報表的格式。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這樣一來,就很可能出現兩個來自不同團隊的程序員分別對Employee類進行修改的情況。不出意外的話,他們各自的修改一定會互相沖突,這就必須要進行代碼合併。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在這個例子中,這次代碼合併不僅有可能讓CTO和COO要求的功能出錯,甚至連CFO原本正常的功能也可能受到影響。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"事實上,這樣的案例還有很多,我們就不一一列舉了。它們的一個共同點是,多人爲了不同的目的修改了同一份源代碼,這很容易造成問題的產生。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"而避免這種問題產生的方法就是將服務不同行爲者的代碼進行切分。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"解決方案","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們有很多不同的方法可以用來解決上面的問題,每一種方法都需要將相關的函數劃分成不同的類。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"其中,最簡單直接的辦法是將數據與函數分離,設計三個類共同使用一個不包括函數的、十分簡單的EmployeeData類,每個類只包含與之相關的函數代碼,互相不可見,這樣就不存在互相依賴的情況了。","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/6a/6a29a1cef77a3544778a15880d144faf.jpeg","alt":"修煉內功:萬字長文詳解面向對象的設計原則","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這種解決方案的壞處在於:程序員現在需要在程序裏處理三個類。另一種解決辦法是使用Facade設計模式。","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/1c/1c4a75f05d68e94f08cb099055792aae.jpeg","alt":"修煉內功:萬字長文詳解面向對象的設計原則","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這樣一來,EmployeeFacade類所需要的代碼量就很少了,它僅僅包含了初始化和調用三個具體實現類的函數。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當然,也有些程序員更傾向於把最重要的業務邏輯與數據放在一起,那麼我們也可以選擇將最重要的函數保留在Employee類中,同時用這個類來調用其他沒那麼重要的函數。","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/2b/2b31ad08bfa55c991d5e002494c531fb.jpeg","alt":"修煉內功:萬字長文詳解面向對象的設計原則","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上圖,將最重要的函數保留在Employee類中,同時調用其他兩個沒那麼重要的類。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"讀者也許會反對上面這些解決方案,因爲看上去這裏的每個類中都只有一個函數。事實上並非如此,因爲無論是計算工資、生成報表還是保存數據都是一個很複雜的過程,每個類都可能包含了許多私有函數。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"總而言之,上面的每一個類都分別容納了一組作用於相同作用域的函數,而在該作用域之外,它們各自的私有函數是互相不可見的。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"單一職責原則主要討論的是函數和類之間的關係——但是它在兩個討論層面上會以不同的形式出現。在組件層面,我們可以將其稱爲共同閉包原則。在軟件架構層面,它則是用於奠定架構邊界的變更軸心。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"OCP:開閉原則","attrs":{}}]},{"type":"horizontalrule","attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"開閉原則(OCP)是Bertrand Meyer在1988年提出[3]的,該設計原則認爲:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"設計良好的計算機軟件應該易於擴展,同時抗拒修改。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"換句話說,一個設計良好的計算機系統應該在不需要修改的前提下就可以輕易被擴展。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"其實這也是我們研究軟件架構的根本目的。如果對原始需求的小小延伸就需要對原有的軟件系統進行大幅修改,那麼這個系統的架構設計顯然是失敗的。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"儘管大部分軟件設計師都已經認可了OCP是設計類與模塊時的重要原則,但是在軟件架構層面,這項原則的意義則更爲重大。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下面,讓我們用一個思想實驗來做一些說明。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"假設我們現在要設計一個在Web頁面上展示財務數據的系統,頁面上的數據要可以滾動顯示,其中負值應顯示爲紅色。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"接下來,該系統的所有者又要求同樣的數據需要形成一個報表,該報表要能用黑白打印機打印,並且其報表格式要得到合理分頁,每頁都要包含頁頭、頁尾及欄目名。同時,負值應該以括號表示。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"顯然,我們需要增加一些代碼來完成這個要求。但在這裏我們更關注的問題是,滿足新的要求需要更改多少舊代碼。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一個好的軟件架構設計師會努力將舊代碼的修改需求量降至最小,甚至爲0。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"但該如何實現這一點呢?我們可以先將滿足不同需求的代碼分組(即SRP),然後再來調整這些分組之間的依賴關係(即DIP)。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"利用SRP,我們可以按下圖中所展示的方式來處理數據流。即先用一段分析程序處理原始的財務數據,以形成報表的數據結構,最後再用兩個不同的報表生成器來產生報表。","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/94/94d963ad0a8212b63435e315df97480d.jpeg","alt":"修煉內功:萬字長文詳解面向對象的設計原則","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"SRP的應用","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這裏的核心就是將應用生成報表的過程拆成兩個不同的操作。即先計算出報表數據,再生成具體的展示報表(分別以網頁及紙質的形式展示)。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"接下來,我們就該修改其源代碼之間的依賴關係了。這樣做的目的是保證其中一個操作被修改之後不會影響到另外一個操作。同時,我們所構建的新的組織形式應該保證該程序後續在行爲上的擴展都無須修改現有代碼。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在具體實現上,我們會將整個程序進程劃分成一系列的類,然後再將這些類分割成不同的組件。下面,我們用下圖中的那些雙線框來具體描述一下整個實現。在這個圖中,左上角的組件是Controller,右上角是Interactor,右下角是Database,左下角則有四個組件分別用於代表不同的Presenter和View。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在下圖中,用<I>標記的類代表接口,用<DS>標記的則代表數據結構;開放箭頭指代的是使用關係,閉合箭頭則指代了實現與繼承關係。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首先,我們在下圖中看到的所有依賴關係都是其源代碼中存在的依賴關係。這裏,從類A指向類B的箭頭意味着A的源代碼中涉及了B,但是B的源代碼中並不涉及A。因此在下圖中,FinancialDataMapper在實現接口時需要知道FinancialDataGateway的實現,而FinancialDataGateway則完全不必知道FinancialDataMapper的實現。","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/e4/e465e9161888ea53969218d3a2991784.jpeg","alt":"修煉內功:萬字長文詳解面向對象的設計原則","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"其次,這裏很重要的一點是這些雙線框的邊界都是單向跨越的。也就是說,上圖中所有組件之間的關係都是單向依賴的,如下圖所示,圖中的箭頭都指向那些我們不想經常更改的組件。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"讓我們再來複述一下這裏的設計原則:如果A組件不想被B組件上發生的修改所影響,那麼就應該讓B組件依賴於A組件。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"所以現在的情況是,我們不想讓發生在Presenter上的修改影響到Controller,也不想讓發生在View上的修改影響到Presenter。而最關鍵的是,我們不想讓任何修改影響到Interactor。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"其中,Interactor組件是整個系統中最符合OCP的。發生在Database、Controller、Presenter甚至View上的修改都不會影響到Interactor。","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/27/27401280d2a1f4b6d1ea43149aa2c898.jpeg","alt":"修煉內功:萬字長文詳解面向對象的設計原則","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"爲什麼Interactor會被放在這麼重要的位置上呢?因爲它是該程序的業務邏輯所在之處,Interactor中包含了其最高層次的應用策略。其他組件都只是負責處理周邊的輔助邏輯,只有Interactor纔是核心組件。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"雖然Controller組件只是Interactor的附屬品,但它卻是Presenter和View所服務的核心。同樣的,雖然Presenter組件是Controller的附屬品,但它卻是View所服務的核心。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"另外需要注意的是,這裏利用“層級”這個概念創造了一系列不同的保護層級。譬如,Interactor是最高層的抽象,所以它被保護得最嚴密,而Presenter比View的層級高,但比Controller和Interactor的層級低。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"以上就是我們在軟件架構層次上對OCP這一設計原則的應用。軟件架構師可以根據相關函數被修改的原因、修改的方式及修改的時間來對其進行分組隔離,並將這些互相隔離的函分組整理成組件結構,使得高階組件不會因低階組件被修改而受到影響。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"依賴方向的控制","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果剛剛的類設計把你嚇着了,別害怕!你剛剛在圖表中所看到的複雜度是我們想要對組件之間的依賴方向進行控制而產生的。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"例如,FinancialReportGenerator和FinancialDataMapper之間的FinancialDataGateway接口是爲了反轉Interactor與Database之間的依賴關係而產生的。同樣的,FinancialReportPresenter接口與兩個View接口之間也類似於這種情況。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當然,FinancialReportRequester接口的作用則完全不同,它的作用是保護FinancialReportController不過度依賴於Interactor的內部細節。如果沒有這個接口,則Controller將會傳遞性地依賴於FinancialEntities。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這種傳遞性依賴違反了“軟件系統不應該依賴其不直接使用的組件”這一基本原則。之後,我們會在討論接口隔離原則和共同複用原則的時候再次提到這一點。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"所以,雖然我們的首要目的是爲了讓Interactor屏蔽掉髮生在Controller上的修改,但也需要通過隱藏Interactor內部細節的方法來讓其屏蔽掉來自Controller的依賴。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"OCP是我們進行系統架構設計的主導原則,其主要目標是讓系統易於擴展,同時限制其每次被修改所影響的範圍。實現方式是通過將系統劃分爲一系列組件,並且將這些組件間的依賴關係按層次結構進行組織,使得高階組件不會因低階組件被修改而受到影響。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"LSP:里氏替換原則","attrs":{}}]},{"type":"horizontalrule","attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1988年,Barbara Liskov在描述如何定義子類型時寫下了這樣一段話:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這裏需要的是一種可替換性:如果對於每個類型是S的對象o1都存在一個類型爲T的對象o2,能使操作T類型的程序P在用o2替換o1時行爲保持不變,我們就可以將S稱爲T的子類型。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"爲了讓讀者理解這段話中所體現的設計理念,也就是里氏替換原則(LSP),我們可以來看幾個例子。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"繼承的使用指導","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"假設我們有一個License類,其結構如下圖所示。該類中有一個名爲calcFee()的方法,該方法將由Billing應用程序來調用。而License類有兩個“子類型”:PersonalLicense與BusinessLicense,這兩個類會用不同的算法來計算授權費用。","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/2f/2fb938ccd0481e40a6ad303883cfba41.jpeg","alt":"修煉內功:萬字長文詳解面向對象的設計原則","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上述設計是符合LSP原則的,因爲Billing應用程序的行爲並不依賴於其使用的任何一個衍生類。也就是說,這兩個衍生類的對象都是可以用來替換License類對象的。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"反例:正方形/長方形問題","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"正方形/長方形問題是一個著名(或者說臭名遠揚)的違反LSP的設計案例","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/98/98c2320a1f40f0eff93da2fbc358a660.jpeg","alt":"修煉內功:萬字長文詳解面向對象的設計原則","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在這個案例中,Square類並不是Rectangle類的子類型,因爲Rectangle類的高和寬可以分別修改,而Square類的高和寬則必須一同修改。由於User類始終認爲自己在操作Rectangle類,因此會帶來一些混淆。例如在下面的代碼中:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Rectangle r = …","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"r.setW(5);","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"r.setH(2);","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"assert(r.area()== 10);","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"很顯然,如果上述代碼在…除返回的是Square類,則最後的這個assert是不會成立的。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果想要防範這種違反LSP的行爲,唯一的辦法就是在User類中增加用於區分Rectangle和Square的檢測邏輯(例如增加if語句)。但這樣一來,User類的行爲又將依賴於它所使用的類,這兩個類就不能互相替換了。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"LSP與軟件架構","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在面向對象這場編程革命興起的早期,我們的普遍認知正如上文所說,認爲LSP只不過是指導如何使用繼承關係的一種方法,然而隨着時間的推移,LSP逐漸演變成了一種更廣泛的、指導接口與其實現方式的設計原則。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這裏提到的接口可以有多種形式——可以是Java風格的接口,具有多個實現類;也可以像Ruby一樣,幾個類共用一樣的方法簽名,甚至可以是幾個服務響應同一個REST接口。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"LSP適用於上述所有的應用場景,因爲這些場景中的用戶都依賴於一種接口,並且都期待實現該接口的類之間能具有可替換性。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"想要從軟件架構的角度來理解LSP的意義,最好的辦法還是來看幾個反面案例","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"違反LSP的案例","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"假設我們現在正在構建一個提供出租車調度服務的系統。在該系統中,用戶可以通過訪問我們的網站,從多個出租車公司內尋找最適合自己的出租車。當用戶選定車子時,該系統會通過調用restful服務接口來調度這輛車。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"接下來,我們再假設該restful調度服務接口的URI被存儲在司機數據庫中。一旦該系統選中了最合適的出租車司機,它就會從司機數據庫的記錄中讀取相應的URI信息,並通過調用這個URI來調度汽車。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"也就是說,如果司機Bob的記錄中包含如下調度URI:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"purplecab.com/driver/Bob","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"那麼,我們的系統就會將調度信息附加在這個URI上,併發送這樣一個PUT請求:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"purplecab.com/driver/Bob","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"/pickupAddress/24 Maple St.","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"/pickupTime/153","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"/destination/ORD","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"很顯然,這意味着所有參與該調度服務的公司都必須遵守同樣的REST接口,它們必須用同樣的方式處理pickupAddress、pickupTime和destination字段。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"接下來,我們再假設Acme出租車公司現在招聘的程序員由於沒有仔細閱讀上述接口定義,結果將destination字段縮寫成了dest。而Acme又是本地最大的出租車公司,另外,Acme CEO的前妻不巧還是我們CEO的新歡……你懂的!這會對系統的架構造成什麼影響呢?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"顯然,我們需要爲系統增加一類特殊用例,以應對Acme司機的調度請求。而這必須要用另外一套規則來構建。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最簡單的做法當然是增加一條if語句:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"if(driver.getDispatchUri().startsWith(\"acme.com\"))…","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"然而很明顯,任何一個稱職的軟件架構師都不會允許這樣一條語句出現在自己的系統中。因爲直接將“acme”這樣的字串寫入代碼會留下各種各樣神奇又可怕的錯誤隱患,甚至會導致安全問題。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"例如,Acme也許會變得更加成功,最終收購了Purple出租車公司。然後,它們在保留了各自名字的同時卻統一了彼此的計算機系統。在這種情況下,系統中難道還要再增加一條“purple”的特例嗎?","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"軟件架構師應該創建一個調度請求創建組件,並讓該組件使用一個配置數據庫來保存URI組裝格式,這樣的方式可以保護系統不受外界因素變化的影響。例如其配置信息可以如下:","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/4d/4d878de77b6e7bbbbc65023b6009e020.png","alt":"修煉內功:萬字長文詳解面向對象的設計原則","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"但這樣一來,軟件架構師就需要通過增加一個複雜的組件來應對並不完全能實現互相替換的restful服務接口。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"LSP可以且應該被應用於軟件架構層面,因爲一旦違背了可替換性,該系統架構就不得不爲此增添大量複雜的應對機制。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"ISP:接口隔離原則","attrs":{}}]},{"type":"horizontalrule","attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/a2/a2df68d8e5e60867fdbcc98e161fc3e6.jpeg","alt":"修煉內功:萬字長文詳解面向對象的設計原則","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在上圖所描繪的應用中,有多個用戶需要操作OPS類。現在,我們假設這裏的User1只需要使用op1,User2只需要使用op2,User3只需要使用op3。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在這種情況下,如果OPS類是用Java編程語言編寫的,那麼很明顯,User1雖然不需要調用op2、op3,但在源代碼層次上也與它們形成依賴關係。這種依賴意味着我們對OPS代碼中op2所做的任何修改,即使不會影響到User1的功能,也會導致它需要被重新編譯和部署。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這個問題可以通過將不同的操作隔離成接口來解決,具體如下圖所示。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"同樣,我們也假設這個例子是用Java這種靜態類型語言來實現的,那麼現在User1的源代碼會依賴於U1Ops和op1,但不會依賴於OPS。這樣一來,我們之後對OPS做的修改只要不影響到User1的功能,就不需要重新編譯和部署User1了。","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/02/02786fd6f488ab605f67442dcf822e60.jpeg","alt":"修煉內功:萬字長文詳解面向對象的設計原則","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"ISP與編程語言","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"很明顯,上述例子很大程度上也依賴於我們所採用的編程語言。對於Java這樣的靜態類型語言來說,它們需要程序員顯式地import、use或者include其實現功能所需要的源代碼。而正是這些語句帶來了源代碼之間的依賴關係,這也就導致了某些模塊需要被重新編譯和重新部署。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"而對於Ruby和Python這樣的動態類型語言來說,源代碼中就不存在這樣的聲明,它們所用對象的類型會在運行時被推演出來,所以也就不存在強制重新編譯和重新部署的必要性。這就是動態類型語言要比靜態類型語言更靈活、耦合度更松的原因。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當然,如果僅僅就這樣說的話,讀者可能會誤以爲ISP只是一個與編程語言的選擇緊密相關的設計原則,而非軟件架構問題,這就錯了。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"ISP與軟件架構","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"回顧一下ISP最初的成因:在一般情況下,任何層次的軟件設計如果依賴於不需要的東西,都會是有害的。從源代碼層次來說,這樣的依賴關係會導致不必要的重新編譯和重新部署,對更高層次的軟件架構設計來說,問題也是類似的。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"例如,我們假設某位軟件架構師在設計系統S時,想要在該系統中引入某個框架F。這時候,假設框架F的作者又將其捆綁在一個特定的數據庫D上,那麼就形成了S依賴於F,F又依賴於D的關係。","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/65/652d38b662d61ddea327cd7f8fc16665.jpeg","alt":"修煉內功:萬字長文詳解面向對象的設計原則","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在這種情況下,如果D中包含了F不需要的功能,那麼這些功能同樣也會是S不需要的。而我們對D中這些功能的修改將會導致F需要被重新部署,後者又會導致S的重新部署。更糟糕的是,D中一個無關功能的錯誤也可能會導致F和S運行出錯。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"DIP:依賴反轉原則","attrs":{}}]},{"type":"horizontalrule","attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"依賴反轉原則(DIP)主要想告訴我們的是,如果想要設計一個靈活的系統,在源代碼層次的依賴關係中就應該多引用抽象類型,而非具體實現。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"也就是說,在Java這類靜態類型的編程語言中,在使用use、import、include這些語句時應該只引用那些包含接口、抽象類或者其他抽象類型聲明的源文件,不應該引用任何具體實現。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"同樣的,在Ruby、Python這類動態類型的編程語言中,我們也不應該在源代碼層次上引用包含具體實現的模塊。當然,在這類語言中,事實上很難清晰界定某個模塊是否屬於“具體實現”。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"顯而易見,把這條設計原則當成金科玉律來加以嚴格執行是不現實的,因爲軟件系統在實際構造中不可避免地需要依賴到一些具體實現。例如,Java中的String類就是這樣一個具體實現,我們將其強迫轉化爲抽象類是不現實的,而在源代碼層次上也無法避免對java.lang.String的依賴,並且也不應該嘗試去避免。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"但String類本身是非常穩定的,因爲這個類被修改的情況是非常罕見的,而且可修改的內容也受到嚴格的控制,所以程序員和軟件架構師完全不必擔心String類上會發生經常性的或意料之外的修改。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"同理,在應用DIP時,我們也不必考慮穩定的操作系統或者平臺設施,因爲這些系統接口很少會有變動。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們主要應該關注的是軟件系統內部那些會經常變動的(volatile)具體實現模塊,這些模塊是不停開發的,也就會經常出現變更。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"穩定的抽象層","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們每次修改抽象接口的時候,一定也會去修改對應的具體實現。但反過來,當我們修改具體實現時,卻很少需要去修改相應的抽象接口。所以我們可以認爲接口比實現更穩定。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"的確,優秀的軟件設計師和架構師會花費很大精力來設計接口,以減少未來對其進行改動。畢竟爭取在不修改接口的情況下爲軟件增加新的功能是軟件設計的基礎常識。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"也就是說,如果想要在軟件架構設計上追求穩定,就必須多使用穩定的抽象接口,少依賴多變的具體實現。下面,我們將該設計原則歸結爲以下幾條具體的編碼守則:","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"應在代碼中多使用抽象接口,儘量避免使用那些多變的具體實現類。這條守則適用於所有編程語言,無論靜態類型語言還是動態類型語言。同時,對象的創建過程也應該受到嚴格限制,對此,我們通常會選擇用抽象工廠(abstract factory)這個設計模式。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"不要在具體實現類上創建衍生類。上一條守則雖然也隱含了這層意思,但它還是值得被單獨拿出來做一次詳細聲明。在靜態類型的編程語言中,繼承關係是所有一切源代碼依賴關係中最強的、最難被修改的,所以我們對繼承的使用應該格外小心。即使是在稍微便於修改的動態類型語言中,這條守則也應該被認真考慮。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"不要覆蓋(override)包含具體實現的函數。調用包含具體實現的函數通常就意味着引入了源代碼級別的依賴。即使覆蓋了這些函數,我們也無法消除這其中的依賴——這些函數繼承了那些依賴關係。在這裏,控制依賴關係的唯一辦法,就是創建一個抽象函數,然後再爲該函數提供多種具體實現。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"應避免在代碼中寫入與任何具體實現相關的名字,或者是其他容易變動的事物的名字。這基本上是DIP原則的另外一個表達方式。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"工廠模式","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果想要遵守上述編碼守則,我們就必須要對那些易變對象的創建過程做一些特殊處理,這樣的謹慎是很有必要的,因爲基本在所有的編程語言中,創建對象的操作都免不了需要在源代碼層次上依賴對象的具體實現。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在大部分面向對象編程語言中,人們都會選擇用抽象工廠模式來解決這個源代碼依賴的問題。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下面,我們通過下圖來描述一下該設計模式的結構。如你所見,Application類是通過Service接口來使用ConcreteImpl類的。然而,Application類還是必須要構造ConcreteImpl類實例。於是,爲了避免在源代碼層次上引入對ConcreteImpl 類具體實現的依賴,我們現在讓 Application 類去調用ServiceFactory接口的makeSvc方法。這個方法就由ServiceFactoryImpl類來具體提供,它是ServiceFactory的一個衍生類。該方法的具體實現就是初始化一個ConcreteImpl類的實例,並且將其以Service類型返回。","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/3c/3cbc107acfccb852695abfd9a8475e0a.jpeg","alt":"修煉內功:萬字長文詳解面向對象的設計原則","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上圖中間的那條曲線代表了軟件架構中的抽象層與具體實現層的邊界。在這裏,所有跨越這條邊界源代碼級別的依賴關係都應該是單向的,即具體實現層依賴抽象層。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這條曲線將整個系統劃分爲兩部分組件:抽象接口與其具體實現。抽象接口組件中包含了應用的所有高階業務規則,而具體實現組件中則包括了所有這些業務規則所需要做的具體操作及其相關的細節信息。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"請注意,這裏的控制流跨越架構邊界的方向與源代碼依賴關係跨越該邊界的方向正好相反,源代碼依賴方向永遠是控制流方向的反轉——這就是DIP被稱爲依賴反轉原則的原因。","attrs":{}}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"具體實現組件","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/36/36f1be9c78f672cde17a67046833d1d9.png","alt":"修煉內功:萬字長文詳解面向對象的設計原則","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在上圖中,具體實現組件的內部僅有一條(紅線)依賴關係,這條關係其實是違反DIP的。這種情況很常見,我們在軟件系統中並不可能完全消除違反DIP的情況。通常只需要把它們集中於少部分的具體實現組件中,將其與系統的其他部分隔離即可。","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"絕大部分系統中都至少存在一個具體實現組件——我們一般稱之爲main組件,因爲它們通常是main函數所在之處。在上圖中,main函數應該負責創建ServiceFactoryImpl實例,並將其賦值給類型爲ServiceFactory的全局變量,以便讓Application類通過這個全局變量來進行相關調用。","attrs":{}}]},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"作者:Robert","attrs":{}}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"原文鏈接:https://mp.weixin.qq.com/s/X6TGkwe-mmtZsS7K4t2_Eg","attrs":{}}]}],"attrs":{}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果覺得本文對你有幫助,可以關注一下我公衆號,回覆關鍵字【面試】即可得到一份Java核心知識點整理與一份面試大禮包!另有更多技術乾貨文章以及相關資料共享,大家一起學習進步!","attrs":{}}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/20/20d8e05b028b03ea8419f639ab2bae6f.png","alt":null,"title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章