設計原則概述
通常來說,要想構建—個好的軟件系統(tǒng)办铡,應該從寫整潔的代碼開始做起。畢竟框喳,如果建筑所使用的磚頭質(zhì)量不佳,那么架構所能起到的作用也會很有限污桦。反之亦然,如果建筑的架構設計不佳巡李,那么其所用的磚頭質(zhì)量再好也沒有用季研。這就是SOLID設計原則所要解決的問題。
SOLID原則的主要作用就是告訴我們?nèi)绾螌?shù)據(jù)和函數(shù)組織成為類,以及如何將這些類鏈接起來成為程序檩奠。請注意桩了,這里雖然用到了“類”這個詞埠戳,但是并不意味著我們將要討論的這些設計原則僅僅適用于面向?qū)ο缶幊獭_@里的類僅僅代表了一種數(shù)據(jù)和函數(shù)的分組,每個軟件系統(tǒng)都會有自己的分類系統(tǒng)屁使,不管它們各自是不是將其稱為“類”在岂,事實上都是SOLID原則的適用領域。
一般情況下蛮寂,我們?yōu)檐浖嫿ㄖ袑咏Y(jié)構的主要目標如下:
- 使軟件可容忍被改動
- 使軟件更容易被理解
- 構建可在多個軟件系統(tǒng)中復用的組件
我們在這里之所以會使用“中層”這個詞蔽午,是因為這些設計原則主要適用于那些進行模塊級編程的程序員。SO凵D原則應該直接緊貼于具體的代碼邏輯之上酬蹋,這些原則是用來幫助我們定義軟件架構中的組件和模塊的及老。
當然了,正如用好磚也會蓋歪樓一樣范抓,采用設計良好的中層組件并不能保證系統(tǒng)的整體架構運作良好骄恶。正因為如此,我們在講完SOLID原則之后匕垫,還會再繼續(xù)針對組件的設計原則進行更進一步的討論叠蝇,將其推進到高級軟件架構部分。
SOLID原則的歷史已經(jīng)很悠久了年缎,早在20世紀80年代末期,我在 USENET新聞組(該新聞組在當時就相當于今天的 Facebook)上和其他人辯論軟件設計理念的時候铃慷,該設計原則就已經(jīng)開始逐漸成型了单芜。隨著時間的推移,其中有一些原則得到了修改犁柜,有一些則被拋棄了洲鸠,還有一些被合并了,另外也增加了一些。它們的最終形態(tài)是在2000年左右形成的扒腕,只不過當時采用的是另外一個展現(xiàn)順序绢淀。
2004年前后, Michael feathers的一封電子郵件提醒我:如果重新排列這些設計原則瘾腰,那么它們的首字母可以排列成SOLID——這就是SOLID原則誕生的故事皆的。
在這一部分中,我們會逐章地詳細討論每個設計原則蹋盆,下面先來做一個簡單摘要费薄。
SRP:單一職責原則。
該設計原則是基于康威定律( Conway‘s Law)的一個推論——軟件系統(tǒng)的最佳結(jié)構高度依賴于開發(fā)這個系統(tǒng)的組織的內(nèi)部結(jié)構栖雾。這樣楞抡,每個軟件模塊都有且只有一個需要被改變的理由。
OCP:開閉原則析藕。
該設計原則是由 Bertrand Meyer在20世紀80年代大力推廣的召廷,其核心要素是:如果軟件系統(tǒng)想要更容易被改變,那么其設計就必須允許新增代碼來修改系統(tǒng)行為账胧,而非只能靠修改原來的代碼竞慢。
LSP:里氏替換原則。
該設計原則是 Barbara liskov在1988年提出的著名的子類型定義找爱。簡單來說梗顺,這項原則的意思是如果想用可替換的組件來構建軟件系統(tǒng),那么這些組件就必須遵守同一個約定车摄,以便讓這些組件可以相互替換寺谤。
ISP:接口隔離原則。
這項設計原則主要告誡軟件設計師應該在設計中避免不必要的依賴吮播。
DIP:依賴反轉(zhuǎn)原則变屁。
該設計原則指出高層策略性的代碼不應該依賴實現(xiàn)底層細節(jié)的代碼,恰恰相反意狠,那些實現(xiàn)底層細節(jié)的代碼應該依賴高層策略性的代碼粟关。
這些年來,這些設計原則在很多不同的出版物中都有過詳細描述环戈。在接下來的章節(jié)中闷板,我們將會主要關注這些原則在軟件架構上的意義,而不再重復其細節(jié)信息院塞。如果你對這些原則并不是特別了解遮晚,那么我建議你先通過腳注中的文檔熟悉一下它們,否則接下來的章節(jié)可能有點難以理解拦止。
SRP:單一職責原則
SRP是SOLID五大設計原則中最容易被誤解的一县遣。也許是名字的原因糜颠,很多程序員根據(jù)SRP這個名字想當然地認為這個原則就是指:每個模塊都應該只做一件事。
沒錯萧求,后者的確也是一個設計原則其兴,即確保一個函數(shù)只完成一個功能。我們在將大型函數(shù)重構成小函數(shù)時經(jīng)常會用到這個原則夸政,但這只是一個面向底層實現(xiàn)細節(jié)的設計原則元旬,并不是SRP的全部。
在歷史上秒梳,我們曾經(jīng)這樣描述SRP這一設計原則:
任何一個軟件模塊都應該有且僅有一個被修改的原因法绵。
在現(xiàn)實環(huán)境中,軟件系統(tǒng)為了滿足用戶和所有者的要求酪碘,必然要經(jīng)常做出這樣那樣的修改朋譬。而該系統(tǒng)的用戶或者所有者就是該設計原則中所指的“被修改的原因”。所以兴垦,我們也可以這樣描述SRP:
任何一個軟件模塊都應該只對一個用戶(User)或系統(tǒng)利益相關者( Stakeholder)負責徙赢。
不過,這里的“用戶”和“系統(tǒng)利益相關者”在用詞上也并不完全準確探越,它們很有可能指的是一個或多個用戶和利益相關者狡赐,只要這些人希望對系統(tǒng)進行的變更是相似的,就可以歸為一類——一個或多有共同需求的人钦幔。在這里枕屉,我們將其稱為行為者( actor)。
所以鲤氢,對于SRP的最終描述就變成了:
任何一個軟件模塊都應該只對某一類行為者負責搀擂。
那么,上文中提剄的“軟件模塊”究竟又是在指什么呢卷玉?大部分情況下哨颂,其最簡單的定義就是指一個源代碼文件。然而相种,有些編程語言和編程環(huán)境并不是用源代碼文件來存儲程序的威恼。在這些情況下,“軟件模塊”指的就是一組緊密相關的函數(shù)和數(shù)據(jù)結(jié)構寝并。
在這里箫措,“相關”這個詞實際上就隱含了SRP這一原則。代碼與數(shù)據(jù)就是靠著與某一類行為者的相關性被組合在一起的衬潦。
或許蒂破,理解這個設計原則最好的辦法就是讓大家來看一些反面案例。
反面案例1:重復的假象别渔。
這是我最喜歡舉的一個例子:某個工資管理程序中的 Employee類有三個函數(shù) calculate Pay()、reportHours()和save()。
[圖片上傳失敗...(image-4b3c15-1615037148047)]
如你所見哎媚,這個類的三個函數(shù)分別對應的是三類非常不同的行為者喇伯,違反了SRP設計原則。
calculatePay()函數(shù)是由財務部門制定的拨与,他們負責向CFO匯報稻据。
reportHours()函數(shù)是由人力資源部門制定并使用的,他們負責向COO匯報买喧。
save()函數(shù)是由DBA制定的捻悯,他們負責向CTO匯報。
這三個函數(shù)被放在同一個源代碼文件淤毛,即同一個Employee類中今缚,程序員這樣做實際上就等于使三類行為者的行為耦合在了一起,這有可能會導致CFO團隊的命令影響到COO團隊所依賴的功能低淡。
例如姓言, calculatePay()函數(shù)和 reportHours()函數(shù)使用同樣的邏輯來計算正常工作時數(shù)。程序員為了避免重復編碼蔗蹋,通常會將該算法單獨實現(xiàn)為個名為 regularHours()的函數(shù)(見下圖)何荚。
接下來餐塘,假設CFO團隊需要修改正常工作時數(shù)的計算方法,而COO帶領的HR團隊不需要這個修改皂吮,因為他們對數(shù)據(jù)的用法是不同的戒傻。
這時候,負責這項修改的程序員會注意到calculate Pay()函數(shù)調(diào)用了 regularHours()函數(shù)涮较,但可能不會注意到該函數(shù)會同時被reportHours()調(diào)用稠鼻。
于是,該程序員就這樣按照要求進行了修改狂票,同時CFO團隊的成員驗證了新算法工作正常候齿。這項修改最終被成功部署上線了。
但是闺属,COO團隊顯然完全不知道這些事情的發(fā)生慌盯,HR仍然在使用 reportHours()產(chǎn)生的報表,隨后就會發(fā)現(xiàn)他們的數(shù)據(jù)出錯了掂器!最終這個問題讓COO十分憤怒亚皂,因為這些錯誤的數(shù)據(jù)給公司造成了幾百萬美元的損失。
與此類似的事情我們肯定多多少少都經(jīng)歷過国瓮。這類問題發(fā)生的根源就是因為我們將不同行為者所依賴的代碼強湊到了一起灭必。對此狞谱,SRP強調(diào)這類代碼一定要被分開。
反面案例2:代碼合并
一個擁有很多函數(shù)的源代碼文件必然會經(jīng)歷很多次代碼合并禁漓,該文件中的這些函數(shù)分別服務不同行為者的情況就更常見了跟衅。
例如,CTO團隊的DBA決定要對 Employee數(shù)據(jù)庫表結(jié)構進行簡單修改播歼。與此同時伶跷,COO團隊的HR需要修改工作時數(shù)報表的格式。
這樣一來秘狞,就很可能出現(xiàn)兩個來自不同團隊的程序員分別對 Employee類進行修改的情況叭莫。不出意外的話,他們各自的修改一定會互相沖突烁试,這就必須要進行代碼合并雇初。
在這個例子中,這次代碼合并不僅有可能讓CTO和COO要求的功能出錯廓潜,甚至連CFO原本正常的功能也可能受到影響抵皱。
事實上,這樣的案例還有很多辩蛋,我們就不一一列舉了呻畸。它們的一個共同點是,多人為了不同的目的修改了同一份源代碼悼院,這很容易造成問題的產(chǎn)生伤为。
而避免這種問題產(chǎn)生的方法就是將服務不同行為者的代碼進行切分。
解決方案
我們有很多不同的方法可以用來解決上面的問題每一種方法都需要將相關的函數(shù)劃分成不同的類据途。
其中绞愚,最簡單直接的辦法是將數(shù)據(jù)與函數(shù)分離,設計三個類共同使用一個不包括函數(shù)的颖医、十分簡單的EmployeeData類(見下圖)位衩,每個類只包含與之相關的函數(shù)代碼,互相不可見熔萧,這樣就不存在互相依賴的情況了糖驴。
這種解決方案的壞處在于:程序員現(xiàn)在需要在程序里處理三個類贮缕。另一種解決辦法是使用 Facade設計模式(見下圖)。
[圖片上傳失敗...(image-164acc-1615037148047)]
這樣一來俺榆, Employee Facade類所需要的代碼量就很少了感昼,它僅僅包含了初始化和調(diào)用三個具體實現(xiàn)類的函數(shù)。
當然罐脊,也有些程序員更傾向于把最重要的業(yè)務邏輯與數(shù)據(jù)放在一起定嗓,那么我們也可以選擇將最重要的函數(shù)保留在 Employee類中蜕琴,同時用這個類來調(diào)用其他沒那么重要的函數(shù)(見下圖)。
讀者也許會反對上面這些解決方案,因為看上去這里的每個類中都只有一個函數(shù)层玲。事實上并非如此,因為無論是計算工資反症、生成報表還是保存數(shù)據(jù)都是一個很復雜的過程辛块,每個類都可能包含了許多私有函數(shù)。
總而言之铅碍,上面的每一個類都分別容納了一組作用于相同作用域的函數(shù)润绵,而在該作用域之外,它們各自的私有函數(shù)是互相不可見的胞谈。
本章小結(jié)
單一職責原則主要討論的是函數(shù)和類之間的關系——但是它在兩個討論層面上會以不同的形式出現(xiàn)尘盼。在組件層面,我們可以將其稱為共同閉包原則( Common Closure Principle)烦绳,在軟件架構層面卿捎,它則是用于奠定架構邊界的變更軸心( Axis of Change)。我們在接下來的章節(jié)中會深入學習這些原則径密。
OCP:開閉原則
開閉原則(OCP)是 Bertrand Meyer在1988年提出的午阵,該設計原則認為:
設計良好的計算機軟件應該易于擴展,同時抗拒修改享扔。
換句話說底桂,一個設計良好的計算機系統(tǒng)應該在不需要修改的前提下就可以輕易被擴展。
其實這也是我們研究軟件架構的根本目的惧眠。如果對原始需求的小小延伸就需要對原有的軟件系統(tǒng)進行大幅修改籽懦,那么這個系統(tǒng)的架構設計顯然是失敗的。
盡管大部分軟件設計師都已經(jīng)認可了OCP是設計類與模塊時的重要原則氛魁,但是在軟件架構層面暮顺,這項原則的意義則更為重大。
下面呆盖,讓我們用一個思想實驗來做一些說明拖云。
思想實驗
假設我們現(xiàn)在要設計一個在Web頁面上展示財務數(shù)據(jù)的系統(tǒng),頁面上的數(shù)據(jù)要可以滾動顯示应又,其中負值應顯示為紅色宙项。
接下來,該系統(tǒng)的所有者又要求同樣的數(shù)據(jù)需要形成一個報表株扛,該報表要能用黑白打印機打印尤筐,并且其報表格式要得到合理分頁汇荐,每頁都要包含頁頭、頁尾及欄目名盆繁。同時掀淘,負值應該以括號表示。
顯然油昂,我們需要增加一些代碼來完成這個要求革娄。但在這里我們更關注的問題是,滿足新的要求需要更改多少舊代碼冕碟。
一個好的軟件架構設計師會努力將舊代碼的修改需求量降至最小拦惋,甚至為0。
但該如何實現(xiàn)這一點呢安寺?我們可以先將滿足不同需求的代碼分組(即SRP)厕妖,然后再來調(diào)整這些分組之間的依賴關系(即DIP)
利用SRP,我們可以按下圖中所展示的方式來處理數(shù)據(jù)流挑庶。即先用一段分析程序處理原始的財務數(shù)據(jù)言秸,以形成報表的數(shù)據(jù)結(jié)構,最后再用兩個不同的報表生成器來產(chǎn)生報表迎捺。
[圖片上傳失敗...(image-b85407-1615037148047)]
這里的核心就是將應用生成報表的過程拆成兩個不同的操作举畸。即先計算出報表數(shù)據(jù),再生成具體的展示報表(分別以網(wǎng)頁及紙質(zhì)的形式展示)破加。
接下來俱恶,我們就該修改其源代碼之間的依賴關系了。這樣做的目的是保證其中一個操作被修改之后不會影響到另外一個操作范舀。同時合是,我們所構建的新的組織形式應該保證該程序后續(xù)在行為上的擴展都無須修改現(xiàn)有代碼。
在具體實現(xiàn)上锭环,我們會將整個程序進程劃分成一系列的類甜刻,然后再將這些類分割成不同的組件全释。下面橄务,我們用下圖中的那些雙線框來具體描述一下整個實現(xiàn)臀栈。在這個圖中,左上角的組件是Controller玫锋,右上角是 Interactor蛾茉,右下角是Database,左下角則有四個組件分別用于代表不同的 Presente和VieW撩鹿。
在圖中谦炬,用“I”標記的類代表接口,用 標記的則代表數(shù)據(jù)結(jié)構;開放箭頭指代的是使用關系,閉合箭頭則指代了實現(xiàn)與繼承關系键思。
[圖片上傳失敗...(image-dafab-1615037148047)]
首先础爬,我們在圖中看到的所有依賴關系都是其源代碼中存在的依賴關系。這里吼鳞,從類A指向類B的箭頭意味著A的源代碼中涉及了B看蚜,但是B的源代碼中并不涉及A。因此在圖中赔桌,F(xiàn)inancialDataMapper在實現(xiàn)接口時需要知道FinancialDataGateway的實現(xiàn)供炎,而FinancialDataGateway則完全不必知道FinancialDataMapper的實現(xiàn)。
其次疾党,這里很重要的一點是這些雙線框的邊界都是單向跨越的碱茁。也就是說,上圖中所有組件之間的關系都是單向依賴的仿贬,如下圖所示,圖中的箭頭都指向那些我們不想經(jīng)常更改的組件墓贿。
讓我們再來復述一下這里的設計原則:如果A組件不想被B組件上發(fā)生的修改所影響聋袋,那么就應該讓B組件依賴于A組件队伟。
所以現(xiàn)在的情況是,我們不想讓發(fā)生在 Presenter上的修改影響到 Controller幽勒,也不想讓發(fā)生在view上的修改影響到 Presenter嗜侮。而最關鍵的是,我們不想讓任何修改影響到 Interactor啥容。
其中锈颗, Interactor組件是整個系統(tǒng)中最符合OCP的。發(fā)生在 Database咪惠、 Controller击吱、 Presenter甚至view上的修改都不會影響到 Interactor。
為什么 interactor會被放在這么重要的位置上呢遥昧?因為它是該程序的業(yè)務邏輯所在之處覆醇, Interactor中包含了其最高層次的應用策略。其他組件都只是負責處理周邊的輔助邏輯炭臭,只有 Interactor才是核心組件永脓。
雖然 Controller組件只是 interactor的附屬品,但它卻是 Presenter和vew所服務的核心鞋仍。同樣的常摧,雖然 Presenter組件是 Controller的附屬品,但它卻是view所服務的核心凿试。
另外需要注意的是排宰,這里利用“層級”這個概念創(chuàng)造了一系列不同的保護層級似芝。譬如, Interactor是最高層的抽象板甘,所以它被保護得最嚴密党瓮,而Presenter比view的層級高,但比 Controller和Interactor的層級低盐类。
以上就是我們在軟件架構層次上對OCP這一設計原則的應用寞奸。軟件架構師可以根據(jù)相關函數(shù)被修改的原因、修改的方式及修改的時間來對其進行分組隔離在跳,并將這些互相隔離的函數(shù)分組整理成組件結(jié)構枪萄,使得高階組件不會因低階組件被修改而受到影響。
依賴方向的控制
如果剛剛的類設計把你嚇著了猫妙,別害怕瓷翻!你剛剛在圖表中所看到的復雜度是我們想要對組件之間的依賴方向進行控制而產(chǎn)生的。
例如割坠,F(xiàn)inancialReportGenerator和FinancialDataMapper之間的FinancialDataGateway接口是為了反轉(zhuǎn) interactor與Database之間的依賴關系而產(chǎn)生的齐帚。同樣的,F(xiàn)inancialReportPresente接口與兩個View接口之間也類似于這種情況彼哼。
信息隱藏
當然对妄, FinancialReportRequester接口的作用則完全不同,它的作用是保護FinancialReportController不過度依賴于Interactor的內(nèi)部細節(jié)敢朱。如果沒有這個接口剪菱,則Controller將會傳遞性地依賴于 Financialentities。
這種傳遞性依賴違反了“軟件系統(tǒng)不應該依賴其不直接使用的組件”這一基本原則拴签。之后孝常,我們會在討論接口隔離原則和共同復用原則的時候再次提到這一點。
所以蚓哩,雖然我們的首要目的是為了讓 Interactor屏蔽掉發(fā)生在 Controller上的修改茫因,但也需要通過隱藏 Interactor內(nèi)部細節(jié)的方法來讓其屏蔽掉來自Controller的依賴。
本章小結(jié)
OCP是我們進行系統(tǒng)架構設計的主導原則杖剪,其主要目標是讓系統(tǒng)易于擴展冻押,同時限制其每次被修改所影響的范圍。實現(xiàn)方式是通過將系統(tǒng)劃分為一系列組件盛嘿,并且將這些組件間的依賴關系按層次結(jié)構進行組織洛巢,使得高階組件不會因低階組件被修改而受到影響。
LSP:里氏替換原則
1988年次兆, Barbara liskov在描述如何定義子類型時寫下了這樣一段話:
這里需要的是一種可替換性:如果對于每個類型是S的對象o1都存在一個類型為T的對象o2稿茉,能使操作T類型的程序P在用o2替換o1時行為保持不變,我們就可以將S稱為T的子類型。
為了讓讀者理解這段話中所體現(xiàn)的設計理念漓库,也就是里氏替換原則(LSP)恃慧,我們可以來看幾個例子。
繼承的使用指導
假設我們有一個 License類渺蒿,其結(jié)構如下圖所示痢士。該類中有一個名為 callee()的方法,該方法將由Billing應用程序來調(diào)用茂装。而 License類有兩個“子類型” :PersonalLicense與 Businesslicense怠蹂,這兩個類會用不同的算法來計算授權費用。
[圖片上傳失敗...(image-881f6d-1615037148047)]
上述設計是符合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類中增加用于區(qū)分 Rectangle和 Square的檢測邏輯(例如增加if語句)妒峦。但這樣一來重斑,User類的行為又將依賴于它所使用的類,這兩個類就不能互相替換了肯骇。
LSP與軟件架構
在面向?qū)ο筮@場編程革命興起的早期窥浪,我們的普遍認知正如上文所說,認為LSP只不過是指導如何使用繼承關系的一種方法笛丙,然而隨著時間的推移漾脂,LSP逐漸演變成了一種更廣泛的、指導接口與其實現(xiàn)方式的設計原則胚鸯。
這里提到的接口可以有多種形式——可以是Java風格的接口骨稿,具有多個實現(xiàn)類;也可以像Ruby一樣,幾個類共用一樣的方法簽名坦冠,甚至可以是幾個服務響應同一個REST接口形耗。
LSP適用于上述所有的應用場景,因為這些場景中的用戶都依賴于一種接口辙浑,并且都期待實現(xiàn)該接口的類之間能具有可替換性激涤。
想要從軟件架構的角度來理解LSP的意義,最好的辦法還是來看幾個反面案例例衍。
違反LSP的案例
假設我們現(xiàn)在正在構建一個提供出租車調(diào)度服務的系統(tǒng)昔期。在該系統(tǒng)中,用戶可以通過訪問我們的網(wǎng)站佛玄,從多個出租車公司內(nèi)尋找最適合自己的出租車硼一。當用戶選定車子時,該系統(tǒng)會通過調(diào)用 restful服務接口來調(diào)度這輛車梦抢。
接下來般贼,我們再假設該 restful調(diào)度服務接口的UR被存儲在司機數(shù)據(jù)庫中。一旦該系統(tǒng)選中了最合適的出租車司機奥吩,它就會從司機數(shù)據(jù)庫的記錄中讀取相應的URI信息哼蛆,并通過調(diào)用這個URI來調(diào)度汽車。
也就是說霞赫,如果司機Bob的記錄中包含如下調(diào)度URI:
purplecab. com/driver/ Bob
那么腮介,我們的系統(tǒng)就會將調(diào)度信息附加在這個URI上,并發(fā)送這樣一個PUT請求:
purplecab. com/driver/Bob
/pickup Address/24 Maple St
/pickupTime/153
/destination/ORD
很顯然端衰,這意味著所有參與該調(diào)度服務的公司都必須遵守同樣的REST接口叠洗,它們必須用同樣的方式處理 pickupAddress、 pickup Time和 destination字段旅东。
接下來灭抑,我們再假設Acme出租車公司現(xiàn)在招聘的程序員由于沒有仔細閱讀上述接口定義,結(jié)果將destination字段縮寫成了dest抵代。而Acme又是本地最大的出租車公司腾节,另外, Acme CEO的前妻不巧還是我們CEO的新歡……你懂的荤牍!這會對系統(tǒng)的架構造成什么影響呢案腺?
顯然,我們需要為系統(tǒng)増加一類特殊用例康吵,以應對Acme司機的調(diào)度請求救湖。而這必須要用另外一套規(guī)則來構建。
最簡單的做法當然是增加一條i語句:
if(driver.getDispatchUri().startsWith(“acme.com))...
然而很明顯涎才,任何一個稱職的軟件架構師都不會允許這樣一條語句出現(xiàn)在自己的系統(tǒng)中鞋既。因為直接將“acme“這樣的字串寫入代碼會留下各種各樣神奇又可怕的錯誤隱患力九,甚至會導致安全問題。
例如邑闺,Acme也許會變得更加成功跌前,最終收購了Purple出租車公司。然后陡舅,它們在保留了各自名字的同時卻統(tǒng)一了彼此的計算機系統(tǒng)抵乓。在這種情況下,系統(tǒng)中難道還要再增加一條“ purple“的特例嗎靶衍?
軟件架構師應該創(chuàng)建一個調(diào)度請求創(chuàng)建組件灾炭,并讓該組件使用一個配置數(shù)據(jù)庫來保存URI組裝格式,這樣的方式可以保護系統(tǒng)不受外界因素變化的影響颅眶。例如其配置信息可以如下
[圖片上傳失敗...(image-ebc5f0-1615037148046)]
但這樣一來蜈出,軟件架構師就需要通過増加一個復雜的組件來應對并不完全能實現(xiàn)互相替換的 restful服務接口。
本章小結(jié)
LSP可以且應該被應用于軟件架構層面涛酗,因為一旦違背了可替換性铡原,該系統(tǒng)架構就不得不為此増?zhí)泶罅繌碗s的應對機制。
ISP:接口隔離原則
“接口隔離原則”這個名字來自下圖所示的這種軟件結(jié)構商叹。
在圖中所描繪的應用中剖笙,有多個用戶需要操作OPS類÷严矗現(xiàn)在,我們假設這里的User1只需要使用op1弥咪,User2只需要使用op2过蹂,User3只需要使用op3。
在這種情況下酪夷,如果OPS類是用Java編程語言編寫的,那么很明顯孽惰,User1雖然不需要調(diào)用op2晚岭、op3,但在源代碼層次上也與它們形成依賴關系勋功。
這種依賴意味著我們對OPS代碼中op2所做的任何修改坦报,即使不會影響到User1的功能,也會導致它需要被重新編譯和部署狂鞋。
這個問題可以通過將不同的操作隔離成接口來解決片择,具體如下圖所示。
同樣啰挪,我們也假設這個例子是用Java這種靜態(tài)類型語言來實現(xiàn)的,那么現(xiàn)在User1的源代碼會依賴于U1Ops和op1嘲叔,但不會依賴于OPS亡呵。這樣一來,我們之后對OPS做的修改只要不影響到User1的功能硫戈,就不需要重新編譯和部署User1了锰什。
ISP與編程語言
很明顯,上述例子很大程度上也依賴于我們所采用的編程語言丁逝。對于Java這樣的靜態(tài)類型語言來說汁胆,它們需要程序員顯式地 Import、use或者 include其實現(xiàn)功能所需要的源代碼霜幼。而正是這些語句帶來了源代碼之間的依賴關系嫩码,這也就導致了某些模塊需要被重新編譯和重新部署。
而對于Ruby和 Python這樣的動態(tài)類型語言來說辛掠,源代碼中就不存在這樣的聲明谢谦,它們所用對象的類型會在運行時被推演出來,所以也就不存在強制重新編譯和重新部署的必要性萝衩。這就是動態(tài)類型語言要比靜態(tài)類型語言更靈活回挽、耦合度更松的原因。
當然猩谊,如果僅僅就這樣說的話千劈,讀者可能會誤以為ISP只是一個與編程語言的選擇緊密相關的設計原則而非軟件架構問題,這就錯了牌捷。
ISP與軟件架構
回顧一下ISP最初的成因:在一般情況下墙牌,任何層次的軟件設計如果依賴于不需要的東西,都會是有害的暗甥。從源代碼層次來說喜滨,這樣的依賴關系會導致不必要的重新編譯和重新部署,對更高層次的軟件架構設計來說撤防,問題也是類似的虽风。
例如,我們假設某位軟件架構師在設計系統(tǒng)S時寄月,想要在該系統(tǒng)中引入某個框架F辜膝。這時候,假設框架F的作者又將其捆綁在一個特定的數(shù)據(jù)庫D上漾肮,那么就形成了S依賴于F厂抖,F(xiàn)又依賴于D的關系。
在這種情況下克懊,如果D中包含了F不需要的功能忱辅,那么這些功能同樣也會是S不需要的七蜘。而我們對D中這些功能的修改將會導致F需要被重新部署,后者又會導致S的重新部署耕蝉。更糟糕的是崔梗,D中一個無關功能的錯誤也可能會導致F和S運行出錯。
本章小結(jié)
本章所討論的設計原則告訴我們:任何層次的軟件設計如果依賴了它并不需要的東西垒在,就會帶來意料之外的麻煩蒜魄。
DIP:依賴反轉(zhuǎn)原則
依賴反轉(zhuǎn)原則(DIP)主要想告訴我們的是,如果想要設計一個靈活的系統(tǒng)场躯,在源代碼層次的依賴關系中就應該多引用抽象類型谈为,而非具體實現(xiàn)。
也就是說踢关,在Java這類靜態(tài)類型的編程語言中伞鲫,在使用use、 Import签舞、 include這些語句時應該只引用那些包含接口秕脓、抽象類或者其他抽象類型聲明的源文件,不應該引用任何具體實現(xiàn)儒搭。
同樣的吠架,在Ruby、 Python這類動態(tài)類型的編程語言中搂鲫,我們也不應該在源代碼層次上引用包含具體實現(xiàn)的模塊傍药。當然,在這類語言中魂仍,事實上很難清晰界定某個模塊是否屬于“具體實現(xiàn)′拐辽。
顯而易見,把這條設計原則當成金科玉律來加以嚴格執(zhí)行是不現(xiàn)實的擦酌,因為軟件系統(tǒng)在實際構造中不可避免地需要依賴到一些具體實現(xiàn)俱诸。例如,Java中的 String類就是這樣一個具體實現(xiàn)赊舶,我們將其強迫轉(zhuǎn)化為抽象類是不現(xiàn)實的睁搭,而在源代碼層次上也無法避免對 java.lang.String的依賴,并且也不應該嘗試去避免锯岖。
但 String類本身是非常穩(wěn)定的介袜,因為這個類被修改的情況是非常罕見的甫何,而且可修改的內(nèi)容也受到嚴格的控制出吹,所以程序員和軟件架構師完全不必擔心String類上會發(fā)生經(jīng)常性的或意料之外的修改。
同理辙喂,在應用DIP時捶牢,我們也不必考慮穩(wěn)定的操作系統(tǒng)或者平臺設施鸠珠,因為這些系統(tǒng)接口很少會有變動。
我們主要應該關注的是軟件系統(tǒng)內(nèi)部那些會經(jīng)常變動的( volatile)具體實現(xiàn)模塊,這些模塊是不停開發(fā)的,也就會經(jīng)常出現(xiàn)變更可缚。
穩(wěn)定的抽象層
我們每次修改抽象接口的時候瓤帚,一定也會去修改對應的具體實現(xiàn)。但反過來赃阀,當我們修改具體實現(xiàn)時搂捧,卻很少需要去修改相應的抽象接口。所以我們可以認為接口比實現(xiàn)更穩(wěn)定索烹。
的確况木,優(yōu)秀的軟件設計師和架枃師會花費很大精力來設計接口,以減少未來對其進行改動。畢竟爭取在不修改接口的情況下為軟件增加新的功能是軟件設計的基礎常識脖捻。
也就是說摩疑,如果想要在軟件架構設計上追求穩(wěn)定楷怒,就必須多使用穩(wěn)定的抽象接口,少依賴多變的具體實現(xiàn)烘贴。下面,我們將該設計原則歸結(jié)為以下幾條具體的編碼守則:
應在代碼中多使用抽象接口逛薇,盡量避免使用那些多變的具體實現(xiàn)類卧秘。這條守則適用于所有編程語言無論靜態(tài)類型語言還是動態(tài)類型語言治专。同時张峰,對象的創(chuàng)建過程也應該受到嚴格限制铣揉,對此额湘,我們通常會選擇用抽象工廠( abstract factory)這個設計模。
不要在具體實現(xiàn)類上創(chuàng)建衍生類箭窜。上一條守則雖然也隱含了這層意思毯焕,但它還是值得被單獨拿出來做次詳細聲明。在靜態(tài)類型的編程語言中,繼承關系是所有一切源代碼依賴關系中最強的纳猫、最難被修改的婆咸,所以我們對繼承的使用應該格外小心。即使是在稍微便于修改的動態(tài)類型語言中芜辕,這條守則也應該被認真考慮尚骄。
不要覆蓋( override)包含具體實現(xiàn)的函數(shù)。調(diào)用包含具體實現(xiàn)的函數(shù)通常就意味著引入了源代碼級別的依賴侵续。即使覆蓋了這些函數(shù)倔丈,我們也無法消除這其中的依賴——這些函數(shù)繼承了那些依賴關系在這里,控制依賴關系的唯一辦法状蜗,就是創(chuàng)建一個抽象函數(shù)需五,然后再為該函數(shù)提供多種具體實現(xiàn)。
應避免在代碼中寫入與任何具體實現(xiàn)相關的名字或者是其他容易變動的事物的名字轧坎。這基本上是DIP原則的另外一個表達方式宏邮。
工廠模式
如果想要遵守上述編碼守則,我們就必須要對那些易變對象的創(chuàng)建過程做一些特殊處理缸血,這樣的謹慎是很有必要的蜀铲,因為基本在所有的編程語言中,創(chuàng)建對象的操作都免不了需要在源代碼層次上依賴對象的具體實現(xiàn)属百。
在大部分面向?qū)ο缶幊陶Z言中记劝,人們都會選擇用抽象工廠模式來解決這個源代碼依賴的問題。
下面族扰,我們通過下圖來描述一下該設計模式的結(jié)構厌丑。如你所見, Application類是通過 Service接口來使用 Concretelmp類的渔呵。然而怒竿, Application類還是必須要構造 Concretelmpl類實例。于是扩氢,為了避免在源代碼層次上引入對 Concretelmpl類具體實現(xiàn)的依賴耕驰,我們現(xiàn)在讓 Application類去調(diào)用ServiceFactory接口的 makeSvc方法。這個方法就由 ServiceFactorylmpl類來具體提供录豺,它是ServiceFactoryl的一個衍生類朦肘。該方法的具體實現(xiàn)就是初始化一個 Concretelmpl類的實例,并且將其以 Service類型返回双饥。
中間的那條曲線代表了軟件架構中的抽象層與具體實現(xiàn)層的邊界咏花。在這里趴生,所有跨越這條邊界源代碼級別的依賴關系都應該是單向的,即具體實現(xiàn)層依賴抽象層。
這條曲線將整個系統(tǒng)劃分為兩部分組件:抽象接口與其具體實現(xiàn)苍匆。抽象接口組件中包含了應用的所有高階業(yè)務規(guī)則刘急,而具體實現(xiàn)組件中則包括了所有這些業(yè)務規(guī)則所需要做的具體操作及其相關的細節(jié)信息。
請注意浸踩,這里的控制流跨越架構邊界的方向與源代碼依賴關系跨越該邊界的方向正好相反叔汁,源代碼依賴方向永遠是控制流方向的反轉(zhuǎn)——這就是DIP被稱為依賴反轉(zhuǎn)原則的原因。
具體實現(xiàn)組件
在上圖中民轴,具體實現(xiàn)組件的內(nèi)部僅有一條依賴關系,這條關系其實是違反DIP的球订。這種情況很常見后裸,我們在軟件系統(tǒng)中并不可能完全消除違反DIP的情況。通常只需要把它們集中于少部分的具體實現(xiàn)組件中冒滩,將其與系統(tǒng)的其他部分隔離即可微驶。
絕大部分系統(tǒng)中都至少存在一個具體實現(xiàn)組件我們一般稱之為main組件,因為它們通常是main函數(shù)所在之處开睡。在圖中因苹,main函數(shù)應該負責創(chuàng)建 ServiceFactorylmp實例,并將其賦值給類型為 ServiceFactory的全局變量篇恒,以便讓 Application類通過這個全局變量來進行相關調(diào)用扶檐。
本章小結(jié)
在系統(tǒng)架構圖中,DIP通常是最顯而易見的組織原則胁艰。我們把圖中的那條曲線稱為架構邊界款筑,而跨越邊界的、朝向抽象層的單向依賴關系則會成為一個設計守則——依賴守則腾么。
以上就是有關設計原則的學習筆記奈梳,希望可以對大家學習設計模式有所幫助,喜歡的小伙伴可以幫忙轉(zhuǎn)發(fā)+關注解虱,感謝大家攘须!Lz也會不定時的更新干貨的!