最近通讀了《架構整潔之道》,受益不淺,遂摘選出設計原則部分,與你們分享,但願你們能從中獲益。java
如下爲書中第3部分 設計原則的原文。程序員
一般來講,要想構建—個好的軟件系統,應該從寫整潔的代碼開始作起。畢竟,若是建築所使用的磚頭質量不佳,那麼架構所能起到的做用也會頗有限。反之亦然,若是建築的架構設計不佳,那麼其所用的磚頭質量再好也沒有用。這就是SOLID設計原則所要解決的問題。算法
SOLID原則的主要做用就是告訴咱們如何將數據和函數組織成爲類,以及如何將這些類連接起來成爲程序。請注意,這裏雖然用到了「類」這個詞,可是並不意味着咱們將要討論的這些設計原則僅僅適用於面向對象編程。這裏的類僅僅表明了一種數據和函數的分組,每一個軟件系統都會有本身的分類系統,無論它們各自是否是將其稱爲「類」,事實上都是SOLID原則的適用領域。數據庫
通常狀況下,咱們爲軟件構建中層結構的主要目標以下:編程
使軟件可容忍被改動設計模式
使軟件更容易被理解數組
構建可在多個軟件系統中複用的組件安全
咱們在這裏之因此會使用「中層」這個詞,是由於這些設計原則主要適用於那些進行模塊級編程的程序員。SO凵D原則應該直接緊貼於具體的代碼邏輯之上,這些原則是用來幫助咱們定義軟件架構中的組件和模塊的。restful
固然了,正如用好磚也會蓋歪樓同樣,採用設計良好的中層組件並不能保證系統的總體架構運做良好。正由於如此,咱們在講完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是SOLID五大設計原則中最容易被誤解的一。也許是名字的緣由,不少程序員根據SRP這個名字想固然地認爲這個原則就是指:每一個模塊都應該只作一件事。
沒錯,後者的確也是一個設計原則,即確保一個函數只完成一個功能。咱們在將大型函數重構成小函數時常常會用到這個原則,但這只是一個面向底層實現細節的設計原則,並非SRP的所有。
在歷史上,咱們曾經這樣描述SRP這一設計原則:
任何一個軟件模塊都應該有且僅有一個被修改的緣由。
在現實環境中,軟件系統爲了知足用戶和全部者的要求,必然要常常作出這樣那樣的修改。而該系統的用戶或者全部者就是該設計原則中所指的「被修改的緣由」。因此,咱們也能夠這樣描述SRP:
任何一個軟件模塊都應該只對一個用戶(User)或系統利益相關者( Stakeholder)負責。
不過,這裏的「用戶」和「系統利益相關者」在用詞上也並不徹底準確,它們頗有可能指的是一個或多個用戶和利益相關者,只要這些人但願對系統進行的變動是類似的,就能夠歸爲一類——一個或多有共同需求的人。在這裏,咱們將其稱爲行爲者( actor)。
因此,對於SRP的最終描述就變成了:
任何一個軟件模塊都應該只對某一類行爲者負責。
那麼,上文中提剄的「軟件模塊」究竟又是在指什麼呢?大部分狀況下,其最簡單的定義就是指一個源代碼文件。然而,有些編程語言和編程環境並非用源代碼文件來存儲程序的。在這些狀況下,「軟件模塊」指的就是一組緊密相關的函數和數據結構。
在這裏,「相關」這個詞實際上就隱含了SRP這一原則。代碼與數據就是靠着與某一類行爲者的相關性被組合在一塊兒的。
或許,理解這個設計原則最好的辦法就是讓你們來看一些反面案例。
反面案例1:重複的假象。
這是我最喜歡舉的一個例子:某個工資管理程序中的 Employee類有三個函數 calculate Pay()、reportHours()和save()。
如你所見,這個類的三個函數分別對應的是三類很是不一樣的行爲者,違反了SRP設計原則。
calculatePay()函數是由財務部門制定的,他們負責向CFO彙報。
reportHours()函數是由人力資源部門制定並使用的,他們負責向COO彙報。
save()函數是由DBA制定的,他們負責向CTO彙報。
這三個函數被放在同一個源代碼文件,即同一個Employee類中,程序員這樣作實際上就等於使三類行爲者的行爲耦合在了一塊兒,這有可能會致使CFO團隊的命令影響到COO團隊所依賴的功能。
例如, calculatePay()函數和 reportHours()函數使用一樣的邏輯來計算正常工做時數。程序員爲了不重複編碼,一般會將該算法單獨實現爲個名爲 regularHours()的函數(見下圖)。
接下來,假設CFO團隊須要修改正常工做時數的計算方法,而COO帶領的HR團隊不須要這個修改,由於他們對數據的用法是不一樣的。
這時候,負責這項修改的程序員會注意到calculate Pay()函數調用了 regularHours()函數,但可能不會注意到該函數會同時被reportHours()調用。
因而,該程序員就這樣按照要求進行了修改,同時CFO團隊的成員驗證了新算法工做正常。這項修改最終被成功部署上線了。
可是,COO團隊顯然徹底不知道這些事情的發生,HR仍然在使用 reportHours()產生的報表,隨後就會發現他們的數據出錯了!最終這個問題讓COO十分憤怒,由於這些錯誤的數據給公司形成了幾百萬美圓的損失。
與此相似的事情咱們確定多多少少都經歷過。這類問題發生的根源就是由於咱們將不一樣行爲者所依賴的代碼強湊到了一塊兒。對此,SRP強調這類代碼必定要被分開。
反面案例2:代碼合併
一個擁有不少函數的源代碼文件必然會經歷不少次代碼合併,該文件中的這些函數分別服務不一樣行爲者的狀況就更常見了。
例如,CTO團隊的DBA決定要對 Employee數據庫表結構進行簡單修改。與此同時,COO團隊的HR須要修改工做時數報表的格式。
這樣一來,就極可能出現兩個來自不一樣團隊的程序員分別對 Employee類進行修改的狀況。不出意外的話,他們各自的修改必定會互相沖突,這就必需要進行代碼合併。
在這個例子中,此次代碼合併不只有可能讓CTO和COO要求的功能出錯,甚至連CFO本來正常的功能也可能受到影響。
事實上,這樣的案例還有不少,咱們就不一一列舉了。它們的一個共同點是,多人爲了避免同的目的修改了同一份源代碼,這很容易形成問題的產生。
而避免這種問題產生的方法就是將服務不一樣行爲者的代碼進行切分。
解決方案
咱們有不少不一樣的方法能夠用來解決上面的問題每一種方法都須要將相關的函數劃分紅不一樣的類。
其中,最簡單直接的辦法是將數據與函數分離,設計三個類共同使用一個不包括函數的、十分簡單的EmployeeData類(見下圖),每一個類只包含與之相關的函數代碼,互相不可見,這樣就不存在互相依賴的狀況了。
這種解決方案的壞處在於:程序員如今須要在程序裏處理三個類。另外一種解決辦法是使用 Facade設計模式(見下圖)。
這樣一來, Employee Facade類所須要的代碼量就不多了,它僅僅包含了初始化和調用三個具體實現類的函數。
固然,也有些程序員更傾向於把最重要的業務邏輯與數據放在一塊兒,那麼咱們也能夠選擇將最重要的函數保留在 Employee類中,同時用這個類來調用其餘沒那麼重要的函數(見下圖)。
讀者也許會反對上面這些解決方案,由於看上去這裏的每一個類中都只有一個函數。事實上並不是如此,由於不管是計算工資、生成報表仍是保存數據都是一個很複雜的過程,每一個類均可能包含了許多私有函數。
總而言之,上面的每個類都分別容納了一組做用於相同做用域的函數,而在該做用域以外,它們各自的私有函數是互相不可見的。
本章小結
單一職責原則主要討論的是函數和類之間的關係——可是它在兩個討論層面上會以不一樣的形式出現。在組件層面,咱們能夠將其稱爲共同閉包原則( Common Closure Principle),在軟件架構層面,它則是用於奠基架構邊界的變動軸心( Axis of Change)。咱們在接下來的章節中會深刻學習這些原則。
開閉原則(OCP)是 Bertrand Meyer在1988年提出的,該設計原則認爲:
設計良好的計算機軟件應該易於擴展,同時抗拒修改。
換句話說,一個設計良好的計算機系統應該在不須要修改的前提下就能夠輕易被擴展。
其實這也是咱們研究軟件架構的根本目的。若是對原始需求的小小延伸就須要對原有的軟件系統進行大幅修改,那麼這個系統的架構設計顯然是失敗的。
儘管大部分軟件設計師都已經承認了OCP是設計類與模塊時的重要原則,可是在軟件架構層面,這項原則的意義則更爲重大。
下面,讓咱們用一個思想實驗來作一些說明。
思想實驗
假設咱們如今要設計一個在Web頁面上展現財務數據的系統,頁面上的數據要能夠滾動顯示,其中負值應顯示爲紅色。
接下來,該系統的全部者又要求一樣的數據須要造成一個報表,該報表要能用黑白打印機打印,而且其報表格式要獲得合理分頁,每頁都要包含頁頭、頁尾及欄目名。同時,負值應該以括號表示。
顯然,咱們須要增長一些代碼來完成這個要求。但在這裏咱們更關注的問題是,知足新的要求須要更改多少舊代碼。
一個好的軟件架構設計師會努力將舊代碼的修改需求量降至最小,甚至爲0。
但該如何實現這一點呢?咱們能夠先將知足不一樣需求的代碼分組(即SRP),而後再來調整這些分組之間的依賴關係(即DIP)
利用SRP,咱們能夠按下圖中所展現的方式來處理數據流。即先用一段分析程序處理原始的財務數據,以造成報表的數據結構,最後再用兩個不一樣的報表生成器來產生報表。
這裏的核心就是將應用生成報表的過程拆成兩個不一樣的操做。即先計算出報表數據,再生成具體的展現報表(分別以網頁及紙質的形式展現)。
接下來,咱們就該修改其源代碼之間的依賴關係了。這樣作的目的是保證其中一個操做被修改以後不會影響到另一個操做。同時,咱們所構建的新的組織形式應該保證該程序後續在行爲上的擴展都無須修改現有代碼。
在具體實現上,咱們會將整個程序進程劃分紅一系列的類,而後再將這些類分割成不一樣的組件。下面,咱們用下圖中的那些雙線框來具體描述一下整個實現。在這個圖中,左上角的組件是Controller,右上角是 Interactor,右下角是Database,左下角則有四個組件分別用於表明不一樣的 Presente和VieW。
在圖中,用「I」標記的類表明接口,用
首先,咱們在圖中看到的全部依賴關係都是其源代碼中存在的依賴關係。這裏,從類A指向類B的箭頭意味着A的源代碼中涉及了B,可是B的源代碼中並不涉及A。所以在圖中,FinancialDataMapper在實現接口時須要知道FinancialDataGateway的實現,而FinancialDataGateway則徹底沒必要知道FinancialDataMapper的實現。
其次,這裏很重要的一點是這些雙線框的邊界都是單向跨越的。也就是說,上圖中全部組件之間的關係都是單向依賴的,以下圖所示,圖中的箭頭都指向那些咱們不想常常更改的組件。
讓咱們再來複述一下這裏的設計原則:若是A組件不想被B組件上發生的修改所影響,那麼就應該讓B組件依賴於A組件。
因此如今的狀況是,咱們不想讓發生在 Presenter上的修改影響到 Controller,也不想讓發生在view上的修改影響到 Presenter。而最關鍵的是,咱們不想讓任何修改影響到 Interactor。
其中, Interactor組件是整個系統中最符合OCP的。發生在 Database、 Controller、 Presenter甚至view上的修改都不會影響到 Interactor。
爲何 interactor會被放在這麼重要的位置上呢?由於它是該程序的業務邏輯所在之處, Interactor中包含了其最高層次的應用策略。其餘組件都只是負責處理周邊的輔助邏輯,只有 Interactor纔是核心組件。
雖然 Controller組件只是 interactor的附屬品,但它倒是 Presenter和vew所服務的核心。一樣的,雖然 Presenter組件是 Controller的附屬品,但它倒是view所服務的核心。
另外須要注意的是,這裏利用「層級」這個概念創造了一系列不一樣的保護層級。譬如, Interactor是最高層的抽象,因此它被保護得最嚴密,而Presenter比view的層級高,但比 Controller和Interactor的層級低。
以上就是咱們在軟件架構層次上對OCP這一設計原則的應用。軟件架構師能夠根據相關函數被修改的緣由、修改的方式及修改的時間來對其進行分組隔離,並將這些互相隔離的函數分組整理成組件結構,使得高階組件不會因低階組件被修改而受到影響。
依賴方向的控制
若是剛剛的類設計把你嚇着了,別懼怕!你剛剛在圖表中所看到的複雜度是咱們想要對組件之間的依賴方向進行控制而產生的。
例如,FinancialReportGenerator和FinancialDataMapper之間的FinancialDataGateway接口是爲了反轉 interactor與Database之間的依賴關係而產生的。一樣的,FinancialReportPresente接口與兩個View接口之間也相似於這種狀況。
信息隱藏
固然, FinancialReportRequester接口的做用則徹底不一樣,它的做用是保護FinancialReportController不過分依賴於Interactor的內部細節。若是沒有這個接口,則Controller將會傳遞性地依賴於 Financialentities。
這種傳遞性依賴違反了「軟件系統不該該依賴其不直接使用的組件」這一基本原則。以後,咱們會在討論接口隔離原則和共同複用原則的時候再次提到這一點。
因此,雖然咱們的首要目的是爲了讓 Interactor屏蔽掉髮生在 Controller上的修改,但也須要經過隱藏 Interactor內部細節的方法來讓其屏蔽掉來自Controller的依賴。
本章小結
OCP是咱們進行系統架構設計的主導原則,其主要目標是讓系統易於擴展,同時限制其每次被修改所影響的範圍。實現方式是經過將系統劃分爲一系列組件,而且將這些組件間的依賴關係按層次結構進行組織,使得高階組件不會因低階組件被修改而受到影響。
1988年, Barbara liskov在描述如何定義子類型時寫下了這樣一段話:
這裏須要的是一種可替換性:若是對於每一個類型是S的對象o1都存在一個類型爲T的對象o2,能使操做T類型的程序P在用o2替換o1時行爲保持不變,咱們就能夠將S稱爲T的子類型。
爲了讓讀者理解這段話中所體現的設計理念,也就是里氏替換原則(LSP),咱們能夠來看幾個例子。
繼承的使用指導
假設咱們有一個 License類,其結構以下圖所示。該類中有一個名爲 callee()的方法,該方法將由Billing應用程序來調用。而 License類有兩個「子類型」 :PersonalLicense與 Businesslicense,這兩個類會用不一樣的算法來計算受權費用。
上述設計是符合LSP原則的,由於 Billing應用程序的行爲並不依賴於其使用的任何一個衍生類。也就是說,這兩個衍生類的對象都是能夠用來替換License類對象的。
正方形/長方形問題
正方形/長方形問題是一個著名(或者說臭名遠揚)的違反LSP的設計案例。
在這個案例中, Square類並非 Rectangle類的子類型,由於 Rectangle類的高和寬能夠分別修改,而 Square類的高和寬則必須一同修改。因爲User類始終認爲本身在操做 Rectangle類,所以會帶來一些混淆。例如在下面的代碼中:
Rectangle r r.setw(5) r.setH(2) assert( rarea()==10)
很顯然,若是上述代碼在…處返回的是 Square類,則最後的這個 assert是不會成立的。
若是想要防範這種違反LSP的行爲,惟一的辦法就是在User類中增長用於區分 Rectangle和 Square的檢測邏輯(例如增長if語句)。但這樣一來,User類的行爲又將依賴於它所使用的類,這兩個類就不能互相替換了。
LSP與軟件架構
在面向對象這場編程革命興起的早期,咱們的廣泛認知正如上文所說,認爲LSP只不過是指導如何使用繼承關係的一種方法,然而隨着時間的推移,LSP逐漸演變成了一種更普遍的、指導接口與其實現方式的設計原則。
這裏提到的接口能夠有多種形式——能夠是Java風格的接口,具備多個實現類;也能夠像Ruby同樣,幾個類共用同樣的方法簽名,甚至能夠是幾個服務響應同一個REST接口。
LSP適用於上述全部的應用場景,由於這些場景中的用戶都依賴於一種接口,而且都期待實現該接口的類之間能具備可替換性。
想要從軟件架構的角度來理解LSP的意義,最好的辦法仍是來看幾個反面案例。
違反LSP的案例
假設咱們如今正在構建一個提供出租車調度服務的系統。在該系統中,用戶能夠經過訪問咱們的網站,從多個出租車公司內尋找最適合本身的出租車。當用戶選定車子時,該系統會經過調用 restful服務接口來調度這輛車。
接下來,咱們再假設該 restful調度服務接口的UR被存儲在司機數據庫中。一旦該系統選中了最合適的出租車司機,它就會從司機數據庫的記錄中讀取相應的URI信息,並經過調用這個URI來調度汽車。
也就是說,若是司機Bob的記錄中包含以下調度URI:
purplecab. com/driver/ Bob
那麼,咱們的系統就會將調度信息附加在這個URI上,併發送這樣一個PUT請求:
purplecab. com/driver/Bob /pickup Address/24 Maple St /pickupTime/153 /destination/ORD
很顯然,這意味着全部參與該調度服務的公司都必須遵照一樣的REST接口,它們必須用一樣的方式處理 pickupAddress、 pickup Time和 destination字段。
接下來,咱們再假設Acme出租車公司如今招聘的程序員因爲沒有仔細閱讀上述接口定義,結果將destination字段縮寫成了dest。而Acme又是本地最大的出租車公司,另外, Acme CEO的前妻不巧仍是咱們CEO的新歡……你懂的!這會對系統的架構形成什麼影響呢?
顯然,咱們須要爲系統増加一類特殊用例,以應對Acme司機的調度請求。而這必需要用另一套規則來構建。
最簡單的作法固然是增長一條i語句:
if(driver.getDispatchUri().startsWith(「acme.com))...
然而很明顯,任何一個稱職的軟件架構師都不會容許這樣一條語句出如今本身的系統中。由於直接將「acme「這樣的字串寫入代碼會留下各類各樣神奇又可怕的錯誤隱患,甚至會致使安全問題。
例如,Acme也許會變得更加成功,最終收購了Purple出租車公司。而後,它們在保留了各自名字的同時卻統一了彼此的計算機系統。在這種狀況下,系統中難道還要再增長一條「 purple「的特例嗎?
軟件架構師應該建立一個調度請求建立組件,並讓該組件使用一個配置數據庫來保存URI組裝格式,這樣的方式能夠保護系統不受外界因素變化的影響。例如其配置信息能夠以下
但這樣一來,軟件架構師就須要經過増加一個複雜的組件來應對並不徹底能實現互相替換的 restful服務接口。
本章小結
LSP能夠且應該被應用於軟件架構層面,由於一旦違背了可替換性,該系統架構就不得不爲此増添大量複雜的應對機制。
「接口隔離原則」這個名字來自下圖所示的這種軟件結構。
在圖中所描繪的應用中,有多個用戶須要操做OPS類。如今,咱們假設這裏的User1只須要使用op1,User2只須要使用op2,User3只須要使用op3。
在這種狀況下,若是OPS類是用Java編程語言編寫的,那麼很明顯,User1雖然不須要調用op二、op3,但在源代碼層次上也與它們造成依賴關係。
這種依賴意味着咱們對OPS代碼中op2所作的任何修改,即便不會影響到User1的功能,也會致使它須要被從新編譯和部署。
這個問題能夠經過將不一樣的操做隔離成接口來解決,具體以下圖所示。
一樣,咱們也假設這個例子是用Java這種靜態類型語言來實現的,那麼如今User1的源代碼會依賴於U1Ops和op1,但不會依賴於OPS。這樣一來,咱們以後對OPS作的修改只要不影響到User1的功能,就不須要從新編譯和部署User1了。
ISP與編程語言
很明顯,上述例子很大程度上也依賴於咱們所採用的編程語言。對於Java這樣的靜態類型語言來講,它們須要程序員顯式地 Import、use或者 include其實現功能所須要的源代碼。而正是這些語句帶來了源代碼之間的依賴關係,這也就致使了某些模塊須要被從新編譯和從新部署。
而對於Ruby和 Python這樣的動態類型語言來講,源代碼中就不存在這樣的聲明,它們所用對象的類型會在運行時被推演出來,因此也就不存在強制從新編譯和從新部署的必要性。這就是動態類型語言要比靜態類型語言更靈活、耦合度更鬆的緣由。
固然,若是僅僅就這樣說的話,讀者可能會誤覺得ISP只是一個與編程語言的選擇緊密相關的設計原則而非軟件架構問題,這就錯了。
ISP與軟件架構
回顧一下ISP最初的成因:在通常狀況下,任何層次的軟件設計若是依賴於不須要的東西,都會是有害的。從源代碼層次來講,這樣的依賴關係會致使沒必要要的從新編譯和從新部署,對更高層次的軟件架構設計來講,問題也是相似的。
例如,咱們假設某位軟件架構師在設計系統S時,想要在該系統中引入某個框架F。這時候,假設框架F的做者又將其捆綁在一個特定的數據庫D上,那麼就造成了S依賴於F,F又依賴於D的關係。
在這種狀況下,若是D中包含了F不須要的功能,那麼這些功能一樣也會是S不須要的。而咱們對D中這些功能的修改將會致使F須要被從新部署,後者又會致使S的從新部署。更糟糕的是,D中一個無關功能的錯誤也可能會致使F和S運行出錯。
本章小結
本章所討論的設計原則告訴咱們:任何層次的軟件設計若是依賴了它並不須要的東西,就會帶來意料以外的麻煩。
依賴反轉原則(DIP)主要想告訴咱們的是,若是想要設計一個靈活的系統,在源代碼層次的依賴關係中就應該多引用抽象類型,而非具體實現。
也就是說,在Java這類靜態類型的編程語言中,在使用use、 Import、 include這些語句時應該只引用那些包含接口、抽象類或者其餘抽象類型聲明的源文件,不該該引用任何具體實現。
一樣的,在Ruby、 Python這類動態類型的編程語言中,咱們也不該該在源代碼層次上引用包含具體實現的模塊。固然,在這類語言中,事實上很難清晰界定某個模塊是否屬於「具體實現′。
顯而易見,把這條設計原則當成金科玉律來加以嚴格執行是不現實的,由於軟件系統在實際構造中不可避免地須要依賴到一些具體實現。例如,Java中的 String類就是這樣一個具體實現,咱們將其強迫轉化爲抽象類是不現實的,而在源代碼層次上也沒法避免對 java.lang.String的依賴,而且也不該該嘗試去避免。
但 String類自己是很是穩定的,由於這個類被修改的狀況是很是罕見的,並且可修改的內容也受到嚴格的控制,因此程序員和軟件架構師徹底沒必要擔憂String類上會發生常常性的或意料以外的修改。
同理,在應用DIP時,咱們也沒必要考慮穩定的操做系統或者平臺設施,由於這些系統接口不多會有變更。
咱們主要應該關注的是軟件系統內部那些會常常變更的( volatile)具體實現模塊,這些模塊是不停開發的,也就會常常出現變動。
穩定的抽象層
咱們每次修改抽象接口的時候,必定也會去修改對應的具體實現。但反過來,當咱們修改具體實現時,卻不多須要去修改相應的抽象接口。因此咱們能夠認爲接口比實現更穩定。
的確,優秀的軟件設計師和架枃師會花費很大精力來設計接口,以減小將來對其進行改動。畢竟爭取在不修改接口的狀況下爲軟件增長新的功能是軟件設計的基礎常識。
也就是說,若是想要在軟件架構設計上追求穩定,就必須多使用穩定的抽象接口,少依賴多變的具體實現。下面,咱們將該設計原則歸結爲如下幾條具體的編碼守則:
應在代碼中多使用抽象接口,儘可能避免使用那些多變的具體實現類。這條守則適用於全部編程語言不管靜態類型語言仍是動態類型語言。同時,對象的建立過程也應該受到嚴格限制,對此,咱們一般會選擇用抽象工廠( abstract factory)這個設計模。
不要在具體實現類上建立衍生類。上一條守則雖然也隱含了這層意思,但它仍是值得被單獨拿出來作次詳細聲明。在靜態類型的編程語言中,繼承關係是全部一切源代碼依賴關係中最強的、最難被修改的,因此咱們對繼承的使用應該格外當心。即便是在稍微便於修改的動態類型語言中,這條守則也應該被認真考慮。
不要覆蓋( override)包含具體實現的函數。調用包含具體實現的函數一般就意味着引入了源代碼級別的依賴。即便覆蓋了這些函數,咱們也沒法消除這其中的依賴——這些函數繼承了那些依賴關係在這裏,控制依賴關係的惟一辦法,就是建立一個抽象函數,而後再爲該函數提供多種具體實現。
應避免在代碼中寫入與任何具體實現相關的名字或者是其餘容易變更的事物的名字。這基本上是DIP原則的另一個表達方式。
工廠模式
若是想要遵照上述編碼守則,咱們就必需要對那些易變對象的建立過程作一些特殊處理,這樣的謹慎是頗有必要的,由於基本在全部的編程語言中,建立對象的操做都免不了須要在源代碼層次上依賴對象的具體實現。
在大部分面向對象編程語言中,人們都會選擇用抽象工廠模式來解決這個源代碼依賴的問題。
下面,咱們經過下圖來描述一下該設計模式的結構。如你所見, Application類是經過 Service接口來使用 Concretelmp類的。然而, Application類仍是必需要構造 Concretelmpl類實例。因而,爲了不在源代碼層次上引入對 Concretelmpl類具體實現的依賴,咱們如今讓 Application類去調用ServiceFactory接口的 makeSvc方法。這個方法就由 ServiceFactorylmpl類來具體提供,它是ServiceFactoryl的一個衍生類。該方法的具體實現就是初始化一個 Concretelmpl類的實例,而且將其以 Service類型返回。
中間的那條曲線表明了軟件架構中的抽象層與具體實現層的邊界。在這裏,全部跨越這條邊界源代碼級別的依賴關係都應該是單向的,即具體實現層依賴抽象層。
這條曲線將整個系統劃分爲兩部分組件:抽象接口與其具體實現。抽象接口組件中包含了應用的全部高階業務規則,而具體實現組件中則包括了全部這些業務規則所須要作的具體操做及其相關的細節信息。
請注意,這裏的控制流跨越架構邊界的方向與源代碼依賴關係跨越該邊界的方向正好相反,源代碼依賴方向永遠是控制流方向的反轉——這就是DIP被稱爲依賴反轉原則的緣由。
具體實現組件
在上圖中,具體實現組件的內部僅有一條依賴關係,這條關係實際上是違反DIP的。這種狀況很常見,咱們在軟件系統中並不可能徹底消除違反DIP的狀況。一般只須要把它們集中於少部分的具體實現組件中,將其與系統的其餘部分隔離便可。
絕大部分系統中都至少存在一個具體實現組件咱們通常稱之爲main組件,由於它們一般是main函數所在之處。在圖中,main函數應該負責建立 ServiceFactorylmp實例,並將其賦值給類型爲 ServiceFactory的全局變量,以便讓 Application類經過這個全局變量來進行相關調用。
本章小結
在系統架構圖中,DIP一般是最顯而易見的組織原則。咱們把圖中的那條曲線稱爲架構邊界,而跨越邊界的、朝向抽象層的單向依賴關係則會成爲一個設計守則——依賴守則。