設計模式六大設計原則

設計模式到底是什麼?它是對整個軟件系統的拆分,組裝,並決定模塊間關係以及如何互動、通信的某種模式。究其本質,設計模式就是以語言特性(面向對象三大特性)為硬件基礎,再加持六大設計原則的靈魂組合而,總結出的一系列套路,本章要講地就是靈魂。

單一職責

  我們知道功能完備的軟件系統是複雜的,系統的拆分與模塊化是不可或缺的,而面向對象是以類來劃分模塊邊界的,也就是說每個類都代表着一個功能角色模塊,其職責應該是單一的,不是自己分內的事不應該負責,這就是單一職責原則。

  舉個例子,燈泡一定是可以亮和滅的,我們定義一個燈泡類並且包含「功率屬性」以及「通電」和「斷電」兩個功能方法,這便是對燈泡的封裝,一對大括號「{}」定義了其類模塊的邊界。

  雖然說我的領域我做主,但絕不可肆意妄為。比如現在客戶要求這個燈泡可以閃爍的霓虹燈效果,我們該怎樣實現?直接在電燈類里再封裝一堆邏輯電路控制其閃爍,比如新加一個flash()方法,並不停來回調用通電斷電?這顯然是錯誤的,燈泡就是燈泡,它只能亮和滅,能不能閃爍不是燈泡的職責,既然進行分類,就不要不倫不類。所以我們需要把閃爍控制電路獨立出來,它們之間的通信應該通過接口去調用,劃清界限,各司其職,這才是類封裝的意義。

  單一職責原則規定,對任何類的修改只能有一個原因,這是由羅伯特·C·馬丁(Robert C. Martin)提出的,這是什麼意思呢?例如我們的燈泡類,它的職責就是照明,與其無關的一切修改動機都不予考慮。所以說燈泡絕不能封裝與其本身職責不相干的功能,這樣就保證其職責的單一性原則,類與類之間有明確的職責劃分,同時也保持一種協作的關係,分與合,對立與統一的辯證關係。最典型的例子例如我們之前講過的」責任鏈模式「中環環相扣審批人的職責範圍就是很好的例子,各顧各的、不管閑事,職責的單一性保證了類的高內聚、低耦合,如此便提高了代碼的易讀性、易維護性、易測試性、易復用性等等。

 

開閉原則

  這個原則聽起來完全不知所云,其實它是簡化命名,其中「開」指的是對擴展是開放的,」閉「則指的是對修改是關閉。通俗來講就是不要修改已有的代碼,而是去寫新的代碼。這對於已經上線並穩定運行的軟件項目來說更為重要,修改代碼的代價是巨大的,小小一個修改有可能會造成整個系統癱瘓,因為其可能會波及到的地方變得不可預知,難以估量。

  舉個簡單的例子,我們有一個筆類用來畫畫,它有一個很簡單的draw方法。這時業務擴展,需要畫各種顏色的畫,難道我們繼續修改這個筆類的draw方法去接受顏色參數並加入大量邏輯判斷嗎?如果後期又需要水彩、水墨、油畫等等顏料效果就需要沒完沒了的對筆進行代碼修改,大量的邏輯代碼會堆積在這個類中,就像拆開封裝的機器殼子對內部電路二次修改,各種導線焊點雜亂無章、臃腫不堪。

  造成這種局面肯定是系統設計上的問題,我們要對其重新審視,對筆類進行抽象,定義好一個繪畫行為draw(),但具體怎樣畫不應予以關心。如此便建立了軟件體系的高層抽象,如果後期要進行擴展,那麼去添加新類並繼承我們的高層抽象即可,各種筆保證了各自的特性,你畫你的,我畫我的。所以說開閉原則是通過抽象去實現的,高層的泛化保證了底層實現的多態化擴展。

  在訪問者模式中對資源模塊的訪問就是非常典型的應用,我們對資源內部並沒有進行邏輯修改,不應該為了附加的功能而修改資源,而是新建訪問者模塊中去實現這些附加功能,今後對系統擴展時我們只需添加新的資源類與訪問者類實現即可,系統模式一旦完美確立就不再修改現有代碼,這就是對擴展的開放,對修改的關閉,添加加比修改好;反之假如系統升級要牽扯進來大量的代碼修改則說明這個設計是失敗的,是違反開閉原則的。其實開閉原則的例子不勝枚舉,對抽象的大量運用奠定了系統的可復用性、可擴展性的基石,增加了系統的穩定性,讀者還需要自己揣摩、體會。

 

里氏替換

  里氏替換原則最早由Barbara Liskov提出的設計模式規範,里氏一詞便來源於其姓氏Liskov,而」替換「指的是父子類的可替換性。此原則指出是任何父類出現的地方子類一定也可以出現,換個角度講也就是說一個優秀的設計中有引用父類的地方,一定可以用子類進行替換。其實面向對象設計語言的特性」繼承與多態「正是為此而生,而我們在設計的時候一定要考慮到這一點,寫框架代碼的時候要面向抽象編程,而不是深入到具體子類中去,這樣才能保證子類多態的可能性。

  假設我們定義有這麼一個類「禽類」,給它加一個飛翔方法fly(),於是客戶端可以自由自在地調用其飛翔方法。不巧某天需要鴕鳥加入禽類的行列,可惜地是鴕鳥並不會飛,這下就鬧得整個雞飛狗跳,此時客戶端就不能調用禽類的飛翔方法了,因為這個禽類有可能就是鴕鳥,這就違反了里氏替換原則。我們意識到最初的設計一定是有問題的,因為不是所有「禽類」都會「飛」,所以對於禽類不該有飛翔方法。

  我們這裡提供一種思路做重構,把禽類的飛翔方法抽離出去給一個接口Flyable,這樣鴕鳥依舊可以繼承禽類,對於其他可以飛的鳥則是繼承禽類並實現Flyable接口。這樣一來,客戶端如果用的是禽類,那一定是鳥而絕不是獸,但不一定能飛,比如是鴕鳥或者火雞;而如果用的是Flyable那它就必然能飛,也許是蝙蝠(獸類)甚至可以是飛機,這些子類一定是在其基類定義範圍內可以隨意替換而不引起任何系統問題。

  之前我們講過的策略模式就是很好的例子,比如我們要使用電腦要進行文檔錄入,電腦會依賴抽象USB接口去讀取數據,至於具體接入什麼錄入設備它不必關心,可以是鍵盤手工錄入,或是掃描儀錄入圖像,只要是兼容USB接口的設備就可以兼容,這就實現了多種USB設備的里氏替換,讓系統功能模塊可靈活替換,可向外延申擴展,這樣的系統才是有設計的,活起來有靈魂的。

 

接口隔離

  接口隔離(分離)指的是對高層接口的獨立、分化,客戶端對類的依賴應該基於最小接口,而不應該依賴不需要的接口。簡單來說就是定義接口的時候盡量往小定義,不要定義成全能型的,什麼都能,什麼都會,最好是一個接口只對應一個角色職能。

  假設我們要定義一個動物高層接口,我們開始思考,區別於植物,動物一定是能跑的,而且能叫,於是我們定義一個移動方法,和一個發聲方法。然後動物們都開始實現這倆方法了,貓跑並喵喵叫;鳥飛並吱吱叫,看似很合理其實不然,有一天兔子蹦蹦跳跳可就是不會叫,但不得不默默地加個啞巴空方法。

  這時就需要反思,接口定義的行為太多了,這些行為定義完全可以拆分開為兩個獨立接口「可移動接口」與「可發聲接口」,這時兔子可以只依賴可移動接口了,而貓則可以依賴一個全新的「又可跑又可發聲」的接口,顯而易見此接口是從那兩個獨立出來的接口繼承來的。

  接口隔離原則要求我們對接口儘可能地細粒度化,小接口總比大接口要好。比如我們都知道Runnable接口,它只要求實現類完成run方法即可,不會把不相干的行為也給加進來,所以它只是定義至其力所能及的範圍,點到為止。其實接口隔離原則與單一職責原則如出一轍,只不過是對高層行為能力的細粒度化規範,這非常好理解,分開的容易合起來,但合起來的就不好分開了,請記住,分開容易合起來難。接口隔離原則能很好地避免接口被設計地過於臃腫,輕量化接口更不會造成對實現類的污染,使系統模塊間依賴變得更加鬆散、靈活。

 

依賴倒置

  依賴倒置是指出只依賴抽象而不依賴具體實現,從而達到降低客戶端對其他模塊耦合的目的。我們知道客戶端類要訪問另一個類,傳統做法是直接訪問其方法,這就導致對實現類的強耦合,而依賴倒置的做法是反其道而行,間接地訪問實現類的高層抽象,依賴高層比依賴底層實現要靈活得多,這也印證了我們在里氏替換里提到的」針對抽象編程「。

  舉個例子,公司CEO制定新一年的策略及目標,為提高產出效率決定年底要上線一套全新的OA辦公自動化軟件。那麼CEO作為客戶端要怎麼實施這個計劃?發動基層程序員們並調用他們的研發方法嗎?我想世界上沒有以這種方式管理公司的CEO吧,作為高層領導一定是調用高層抽象,大手一揮調用IT部門接口的work方法並傳入目標即可。至於這個work方法的實現是公司程序員去研發寫代碼實現,或是找外包公司項目承包,甚至是直接找成熟的產品直接購買,CEO完全不必操心,這時就達到了與具體實現類解耦的目的,不合適還可以隨意靈活替換,這就是把「依賴底層」倒置為「依賴高層」的好處。

  我們在做開發的時候常常會從高層往底層寫代碼,例如從業務邏輯層的時候我們大可不必過多關心數據源是什麼,是文件還是數據庫,是MySQL還是Oracle。所以我們可以調用數據訪問接口,而其實現類可以暫且不寫或者寫一個模擬實現類用來單元測試,甚至可以交給熟悉的同事並行開發,只要是定義了良好的接口規範就不必關心底層實現細節,依賴高層抽象,不依賴底層具像,這就是依賴倒置原則的核心思想,從具象到抽象的倒置。

 

迪米特法則

  迪米特法則或者被稱為最少知識原則主要是通過最小化各模塊間的通信、而割裂模塊間千絲萬縷的不必要聯繫,以達到松耦合的目的。迪米特法則提出一個模塊對其他模塊要知之甚少、拒絕陌生人、只和熟人交談,否則對一個類的變動將引發蝴蝶效應般的連鎖反應,這會波及到大範圍的變動,系統可維護性差。

  舉個例子,我們買了一台遊戲機,它像一給黑盒子內部集成了非常複雜的電路以及各種電子元件並且對外開放了手柄控制接口,這便是一個完美的封裝。對於我們用戶來說只需要用手柄操作就可以了,至於其內部的那些磁盤載入、內存讀取、CPU指令接收、顯卡顯示等等我們是完全陌生的,也並不會去直接調用,這就是用戶的正確使用方法。

  再以門面模式為例,我們如果去辦理一項業務,在業務大廳里要排隊、填表、遞交、蓋章等等。這麼一來我們就得了解每個窗口所需哪些材料,怎麼樣的辦理流程,對於一個從來沒辦理過這個業務的人來說一定會來來回回折騰。對於這種陌生的事務處理應該交給專業的接待員這個」門面「來解決,這個角色與導遊有異曲同工之秒,我們只需簡單地把材料遞交給他們就行了,我們只和」門面「通信,至於門面怎樣去走的流程我們知之甚少,這是門面封裝好的內部事務,我們更沒有必要去親自處理。

  此外還有像中介模式、適配器模式等等都好像是給陌生人搭橋一樣的松耦合典範。系統模塊應該隱藏內部機制,大門一定要緊鎖,防止陌生人隨意訪問,而對外只暴露適度地接口,這樣才能保證模塊間的最少知識通信,切勿越級彙報,禁止跨界、干涉他人內務,讓模塊間調用變得」傻瓜化「,即開即用,使模塊間降低耦合性,提高軟件系統的可維護性、可擴展性。

 

常道

  軟件設計絕不能不切實際而刻板生硬地套用模式,其實有時並不適用,也許本來幾個類就可以解決的的需求非要拆成幾十個角色類,結果適得其反,很簡單的一個系統搞得臃腫不堪,生搬硬套的設計模式反倒變成一種雞肋。其實各設計模式之間都是有共通之處的,有些看起來十分類似但又能解決不同的問題,套路當然有類似之處了,即便作為靈魂的設計原則也隱隱約約有着千絲萬縷的關聯,其實他們往往是相輔相成、互相印證的。所以我們不必過度糾結,把他們機械式地分門別類、劃清界限。需求雖然是多變的,但一個系統不可能不做修改就滿足所有變化,我們需根據當下以及可以預估的未來變更運用恰當的模式,適可而止,以不變應萬變才不至於過度設計,模式泛濫。

  或許工作多年後我們忘掉了那些設計模式的定義,又或者把模式名字混淆了,其實對設計模式的思想真諦來說,它叫什麼名就顯得不那麼重要了,正所謂「道可道,非常道;名可名, 非常名。」,在實際應用中能快速解決當下問題才是最務實的工作態度。直到有一天,我們設計出的系統也許用到了某個模式的變種,又或許是幾個設計模式巧妙地組合運用,被問及運用了何種設計模式時答曰「無名」。

  當我們突招式的牽絆,概念的劃分邊界變得模糊,你中有我我中有你的套路變得渾然一體,這才是真正做到了得心應手、揮灑自如。真正的高手一定是手中無劍,心中有劍,達到無劍勝有劍,無招勝有招的最高境界。

 

 

原文參考公眾號【Java知音】