经典永不过时!重温设计模式
| 導語?在軟工程中,設計模式(design pattern)是對軟件設計中普遍存在(反復出現(xiàn))的各種問題,所提出的解決方案。這個術(shù)語是由埃里希·伽瑪(Erich Gamma)等人在1990年代從建筑設計領(lǐng)域引入到計算機科學的,設計模式是針對軟件設計中常見問題的工具箱,其中的工具就是各種經(jīng)過實踐驗證的解決方案。即使你從未遇到過這些問題,了解模式仍然非常件有用,因為它能指導你如何使用面向?qū)ο蟮脑O計原則來解決各種問題。
大家好,我是Alex,今天談一談設計模式,一名優(yōu)秀的開發(fā),應該多少都需要了解一些常用的設計模式和使用場景,讓我們一起來重溫一下那些年經(jīng)典設計模式;
本文主要內(nèi)容
為什么要掌握設計模式
歷史的教訓
時間回到 20 世紀 80 年代,當時的軟件行業(yè)正處于第二次軟件危機中。根本原因是,隨著軟件規(guī)模和復雜度的快速增長,如何高效高質(zhì)的構(gòu)建和維護這樣大規(guī)模的軟件成為了一大難題。無論是開發(fā)何種軟件產(chǎn)品,成本和時間都最重要的兩個維度。較短的開發(fā)時間意味著可比競爭對手更早進入市場;較低的開發(fā)成本意味著能夠留出更多營銷資金,因此能更廣泛地覆蓋潛在客戶。
設計模式是銀彈嗎?
代碼復用是減少開發(fā)成本,減低復雜度最常用的方式之一,這個想法表面看起來很棒,但實際上要讓已有代碼在全新的上下文中工作,通常還是需要付出額外努力的。組件間緊密的耦合、對具體類而非接口的依賴和硬編碼的行為都會降低代碼的靈活性,使得復用這些代碼變得更加困難。設計模式目標就是幫助軟件提高內(nèi)聚,減低耦?????????????????合,?????????????????使用設計模式是增加軟件組件靈活性并使其易于復用的方式之一。
變化是程序員生命中唯一不變的事情,客戶需求可能經(jīng)常會變,緊急上線的版本,要不要下次重構(gòu)一下,還是繼續(xù)打各種補丁, 技術(shù)債會越積越多,因此在設計程序架構(gòu)時,所有有經(jīng)驗的開發(fā)者會盡量選擇支持未來任何可能變更的方式。可擴展性成為了程序設計必須要考慮指標,而設計模式是可以借鑒的,成熟的優(yōu)化程序設計的解決方案;
總體來說,深刻理解設計模式會給我們帶來很多好處:
可以和面試官"暢談"設計模式相關(guān)問題.
很多開源軟件框架大量使用了設計模式,比如Linux系統(tǒng),Redis,Spring,C++STL等等,可以把幫你加快理解開源軟件框架。
當你寫的代碼越來越優(yōu)美后,你的代碼鑒賞能力就會提高,對團隊code review貢獻也會更大,在個人影響力也會提高。
你不會再畏手畏腳,你的工具箱里面工具很多后,可以幫助你,應對各種大型項目的代碼設計和開發(fā)。
每個領(lǐng)域都會一些成熟"套路", 編程也不例外,熟悉這些套路,可以更好方便交流和更快速地解決問題;
為了更好理解設計模式,我們首先要理解一些重要的設計原則,而不是片面理解設計模式哪些模式名詞,要看清楚這背后的原理,這個才是最重要的。
代碼設計原則
代碼設計原則貫穿在整個設計模式之中,是理解其中的精華,本文討論了一些重要的設計原則,包括通用設計原則,DRY原則,KISS原則,SOLID原則等:
通用設計原則
隔離變化
找到程序中的變化內(nèi)容并將其與不變的內(nèi)容區(qū)分開,該原則的主要目的是將變更造成的影響最小化。
面向接口編程
面向接口進行開發(fā), 而不是面向?qū)崿F(xiàn);依賴于抽象類型,而不是具體類,要求接口標準化設計,只要對外的接口沒有變,內(nèi)部實現(xiàn)就可以任意變化,為以后留有更多優(yōu)化空間,方便以后更新迭代,可以說這樣的設計是靈活的。
組合優(yōu)于繼承
繼承可能是類之間最明顯、最簡便的代碼復用方式。如果你有兩個代碼相同的類, 就可以為它們創(chuàng)建一個通用的基類,然后將相似的代碼移動到其中。但繼承可能帶來的問題:
子類不能減少超類的接口。你必須實現(xiàn)父類中所有的抽象方法,即使它們沒什么用。
在重寫方法時,你需要確保新行為與其基類中的版本兼容。這一點很重要,因為子類的所有對象都可能被傳遞給以超類對象為參數(shù)的任何代碼,相信你不會希望這些代碼崩潰的。
繼承打破了超類的封裝,因為子類擁有訪問父類內(nèi)部詳細內(nèi)容的權(quán)限。此外還可能會有相反的情況出現(xiàn),那就是程序員為了進一步擴展的方便而讓超類知曉子類的內(nèi)部詳細內(nèi)容。
子類與超類緊密耦合。超類中的任何修改都可能會破壞子類的功能。
通過繼承復用代碼可能導致平行繼承體系的產(chǎn)生。繼承通常僅發(fā)生在一個維度中。只要出現(xiàn)了兩個以上的維度,你就必須創(chuàng)建數(shù)量巨大的類組合,從而使類層次結(jié)構(gòu)膨脹到不可思議的程度。
組合是代替繼承的一種方法。繼承代表類之間的“是”關(guān)系(汽車是交通工具),而組合則代表“有”關(guān)系(汽車有一個引擎)。
DRY 原則
DRY-Don't Repeat Yourself(不要重復代碼)
降低可管理單元的復雜度的基本策略是將系統(tǒng)分成多個部分。
理解這一原理是如此重要,它通常以首字母縮寫詞DRY來指代,并出現(xiàn)在Andy Hunt和Dave Thomas的書《實用程序員》中,但是這個概念本身已經(jīng)有很長時間了。它指的是軟件的最小部分。
當您構(gòu)建一個大型軟件項目時,通常會因整體復雜性而感到不知所措。人類不善于管理復雜性;他們擅長為特定范圍的問題找到有創(chuàng)意的解決方案。降低可管理單元的復雜性的基本策略是將系統(tǒng)分成更方便的部分。首先,您可能希望將系統(tǒng)分為多個組件,其中每個組件代表其自己的子系統(tǒng),其中包含完成特定功能所需的一切。
KISS 原則
KISS是使它保持簡單,愚蠢的首字母縮寫,是美國海軍在1960年提出的設計原則。KISS原則指出,大多數(shù)系統(tǒng)如果保持簡單而不是變得復雜,則效果最佳。因此,簡單性應該是設計的主要目標,并且應該避免不必要的復雜性。
SOLID 原則
SOLID 原則是在羅伯特·馬丁的著作《敏捷軟件開發(fā):原則、模式與實踐》中首次提出的,SOLID 是讓軟件設計更易于理解、更加靈活和更易于維護的五個原則的簡稱。
盡量讓每個類或者函數(shù)只負責軟件中的一個功能,這條原則的主要目的是減少復雜度,你不需要費盡心機地去構(gòu)思如何僅用200 行代碼來實現(xiàn)復雜設計,實際上完全可以使用十幾個清晰的方法,這里核心是:?通過實現(xiàn)最基本"原子函數(shù)", 其他復雜功能都可以通過這些原子函數(shù)構(gòu)建,每一層的函數(shù)語義都是單一的,通過層層封裝,最終構(gòu)建一個龐大可控的系統(tǒng)。
?
本原則的主要理念是在實現(xiàn)新功能時能保持已有代碼不變,為什么呢,主要是修改存量代碼,很可能會影響軟件穩(wěn)定性,很多線上代碼跑了好多年了,經(jīng)歷很多輪迭代,各種補丁,如果考慮不全面,很容易帶來風險,下圖比較形象說明:
替換原則是用于預測子類是否與代碼兼容,以及是否能與其超類對象協(xié)作的一組檢查。這一概念在開發(fā)程序庫和框架時非常重要, 因為其中的類將會在他人的代碼中使用——你是無法直接訪問和修改這些代碼的。里氏替換原則的重點在不影響原功能。
根據(jù)接口隔離原則,你必須將“臃腫”的方法拆分為多個顆粒度更小的具體方法。客戶端必須僅實現(xiàn)其實際需要的方法。否則,對于“臃腫”接口的修改可能會導致程序出錯,即使客戶端根本沒有使用修改后的方法。
通常在設計軟件時,你可以辨別出不同層次的類。
? 低層次的類實現(xiàn)基礎操作(例如磁盤操作、傳輸網(wǎng)絡數(shù)據(jù)和連接數(shù)據(jù)庫等)。
? 高層次類包含復雜業(yè)務邏輯以指導低層次類執(zhí)行特定操作。
經(jīng)典設計模式
這里列舉了22種設計模式,大致分為三類:創(chuàng)建型模式,結(jié)構(gòu)型模式,行為模式;
創(chuàng)建型模式提供創(chuàng)建對象的機制,增加已有代碼的靈活性和可復用性
結(jié)構(gòu)型模式介紹如何將對象和類組裝成較大的結(jié)構(gòu),并同時保持結(jié)構(gòu)的靈活和高效:
行為模式負責對象間的高效溝通和職責委派:
推薦一個經(jīng)典學習網(wǎng)站:
https://refactoring.guru
上面每種模式配有形象圖,比如工廠方法模式:
?
還提供對應的設計類圖:
也提供了對應代碼示例:
支持9種語言的實現(xiàn):
? ? 代碼在:https://github.com/RefactoringGuru
推薦給大家,拿走別謝;
更多請參考:
《設計模式:可復用面向?qū)ο筌浖幕A》
? https://refactoring.guru
Linux經(jīng)典設計模式
?內(nèi)核面向?qū)ο笤O計模式
Linux雖然是面向過程的c語言寫成的,但是卻可以表達面向?qū)ο蟮乃枷?#xff0c;Linux內(nèi)核大量使用面向?qū)ο蟮木幋a風格,我們可以從中至少學習到兩點:
說明在大型軟件開發(fā)中,OOP編程思想很重要,和具體語言無關(guān);
同時展示了怎么用c語言實現(xiàn)OOP編程,值得廣大C語言開發(fā)者學習。
我們用例子來說明。
封裝
以內(nèi)核proto定義為例:
struct proto 定義傳輸層接口方法和相應成員數(shù)據(jù),類似C++的class定義;可以根據(jù)這個class生產(chǎn)很多實例,比如TCP實例,可以通過統(tǒng)一接口訪問TCP實例的方法和數(shù)據(jù)。
繼承
以內(nèi)核套接字體系為例:
基于此繼承體系,對于一些接受 struct sock* 形參的接口,就可以直接把上述的子類套接字實例 struct udp_sock* sk作為實參傳進去(當然,這里需要指針強轉(zhuǎn)一次(struct sock*)sk)。這里就是OOP中“is a"的public繼承關(guān)系,子類對象可以直接作為父類對象使用,并且這種實現(xiàn)只支持單繼承。
多態(tài)
用C實現(xiàn)多態(tài)需要自己維護繼承關(guān)系中的虛函數(shù)體系,C++有編譯器自動生成、維護vtbl與vptr。Linux內(nèi)核的實現(xiàn)中,將系列函數(shù)指針放入結(jié)構(gòu)體,即視其為“虛函數(shù)”,亦或是專門定義一個xxx_ops結(jié)構(gòu),里面放上一堆函數(shù)指針,作為“虛函數(shù)表”。仍以套接字體系為例,在基類 sock 中,有協(xié)議結(jié)構(gòu)體指針 struct proto *skc_prot; 這個proto即可大體上視為一個虛函數(shù)表vtbl,內(nèi)有具體協(xié)議的函數(shù)指針,而這個skc_prot指針,即可視為虛指針vptr。
在套接字創(chuàng)建時,根據(jù)參數(shù)中的協(xié)議族、協(xié)議類型、協(xié)議號信息,調(diào)用協(xié)議族的create函數(shù)執(zhí)行創(chuàng)建,綁定具體協(xié)議proto指針到該vptr上,自此實現(xiàn)了靜態(tài)類型到動態(tài)類型的綁定。之后,當調(diào)用虛函數(shù)時,即可直接通過這些函數(shù)指針進行多態(tài)的調(diào)用 , 比如下面例子socket調(diào)用connect接口:
這里第一個參數(shù)sk即可看做this指針,不同socket對象,會訪問對應協(xié)議接口,從而實現(xiàn)多態(tài)訪問:
list 設計模式
list作為常用數(shù)據(jù)結(jié)構(gòu),寫代碼時候經(jīng)常會遇到,可以看一下傳統(tǒng)list設計和內(nèi)核list設計有什么不一樣。
一般的雙向鏈表一般是如下的結(jié)構(gòu):
有個單獨的頭結(jié)點(head)
每個節(jié)點(node)除了包含必要的數(shù)據(jù)之外,還有2個指針(pre,next)
pre指針指向前一個節(jié)點(node),next指針指向后一個節(jié)點(node)
頭結(jié)點(head)的pre指針指向鏈表的最后一個節(jié)點
最后一個節(jié)點的next指針指向頭結(jié)點(head)
傳統(tǒng)list如下圖:
傳統(tǒng)的鏈表不同node類型,需要重新定義結(jié)構(gòu),不夠通用化,還需要為node實現(xiàn)脫鏈、入鏈操作等。
我們需要抽象出一個“基類”來實現(xiàn)鏈表的功能,其他數(shù)據(jù)結(jié)構(gòu)只需要簡單的繼承這個鏈表類就可以了。
內(nèi)核list設計如下:
鏈表不是將用戶數(shù)據(jù)保存在鏈表節(jié)點中,而是將鏈表節(jié)點保存在用戶數(shù)據(jù)中
鏈表節(jié)點只有2個指針(prev和next)
prev指針指向前一個節(jié)點的鏈表節(jié)點,next指針指向后一個節(jié)點(node)的鏈表節(jié)點
如下圖:
這樣設計的好處是鏈表的節(jié)點將獨立于用戶數(shù)據(jù)之外,便于把鏈表的操作獨立出來,和具體數(shù)據(jù)節(jié)點無關(guān),這里可能有些人會問,數(shù)據(jù)節(jié)點怎么訪問呢?? 內(nèi)核通過一個container_of的宏從鏈表節(jié)點找到數(shù)據(jù)節(jié)點起始地址:
找到數(shù)據(jù)節(jié)點起始地址后,通過數(shù)據(jù)節(jié)點定義就可以訪問數(shù)據(jù)了,內(nèi)核紅黑樹rbtree也是同樣的設計。
設備驅(qū)動框架設計模式
?
從Linux2.6開始Linux加入了一套驅(qū)動管理和注冊機制—platform平臺總線驅(qū)動模型:
當調(diào)用platform_device_register(或platform_driver_register)注冊platform_device(或platform_driver)時,首先會將其加入platform總線上,依次匹配platform總線上的platform_driver(或platform_device),然后調(diào)用platform_driver的.probe函數(shù)。其中platform_device存放設備資源(硬件息息相關(guān)代碼,易變動),platform_driver則使用資源(比較穩(wěn)定的代碼),這樣當改動硬件資源時,我們的上層使用資源的代碼部分幾乎可以不用去改動。
這里設計通過中間bus層,把強耦合Device和對應Driver進行了解耦隔離,定好match,probe等標準通信接口,就可以獨立開發(fā),通過總線bus進行關(guān)聯(lián)通信,有點類似中介模式。
C++ Idioms(設計習語)
由于篇幅優(yōu)先,這里列舉一些非常重要且非常實用的C++專有的設計模式。
RAII-Resource Acquisition Is Initialization
‘資源獲取即初始化‘(簡稱 RAII)是C++防止內(nèi)存泄露一個很好解決方案,它結(jié)合構(gòu)造函數(shù)和析構(gòu)函數(shù),把資源生命周期和對象生命周期綁定起來,在構(gòu)造函數(shù)中獲取資源(這些錯誤會引發(fā)異常),然后將其釋放到析構(gòu)函數(shù)中(永不拋出),并且不需要顯式清理,從而防止忘記釋放資源;
?
C ++STL庫很多類遵循RAII設計原則,比如std :: string,std :: vector,std :: thread等。
Policy-based class?Design
基于策略設計又名policy-based class design 是一種基于C++計算機程序設計模式,以策略(Policy)為基礎,并結(jié)合C++的模板元編程。就是將原本復雜的系統(tǒng),拆解成多個獨立運作的“策略類別”,每一組policy class都只負責單純?nèi)缧袨榛蚪Y(jié)構(gòu)的某一方面。多重繼承由于繼承自多組 Base Class,故缺乏型別消息,而Templetes基于型別,擁有豐富的型別消息。多重繼承容易擴張,而Templetes的特化不容易擴張。Policy-Based Class Design 同時使用了 Template 以及 Multiple Inheritance 兩項技術(shù),結(jié)合兩者的優(yōu)點,看下面例子:
ResourceManager則稱為宿主類別(host class),只需要切換不同 Policy Class(ReadPolicy or WritePolicy),就可以得到不同的功能實體。Policy不一定要被宿主繼承,只需要用委托完成這一工作。但policies必須遵守一個隱含的constraint,接口必須一樣,故參數(shù)不能有巨大改變,policy 的一個重要的特征是,宿主類別經(jīng)常(并不一定要)使用多重繼承的機制去使用多個 policy classes. 因此在進行 policy 拆解時,必須要盡可能達成正交分解,policy之間最好彼此獨立運作,不相互影響。Pimpl - Pointer to implementation
Pimpl是一種廣泛使用的削減編譯依賴項的技術(shù), 看下面例子可能就明白了:
??
因為Widget的成員變量有std::string,std::vector和Gadget,那么這些類型的頭文件在Widget編譯時必須出現(xiàn),這意味Widget的用戶必須包含“gadget.h”。這些增加的頭文件會增加Widget用戶的編譯時間,而且這使得用戶依賴于這些頭文件,即如果某個頭文件的內(nèi)容被改變了,Widget的用戶就要重新編譯。標準庫頭文件不會經(jīng)常改變,但是“gadget.h”可能會經(jīng)常修改。所以需要Pimp技術(shù)來消除這種變化影響--隔離變化;
?
這樣Widget頭文件里面就不需要包含“gadget.h”文件了,再CPP文件中再聲明具體的類型:
??
在這里,我展示了“#include”指令,只為了說明所有對頭文件的依賴(即std::string,std::vector和Gadget)依然存在。不過呢,依賴已經(jīng)從“widget.h”(Widget用戶可見的和使用的)轉(zhuǎn)移到“widget.cpp”(只有Widget的實現(xiàn)者?????????????????????????才能看見和使用),這樣就把widget頭文件變化影響隔離在內(nèi)部實現(xiàn)中,對外接口不變,這里就體會到這種設計模???????????????式的好處。
CRTP -The curiously recurring template pattern?
CRTP (奇異遞歸模板模式)是一種在編譯期實現(xiàn)多態(tài)方法,是對運行時多態(tài)一種優(yōu)化,多態(tài)是個很好的特性,但是動態(tài)綁定比較慢,因為要查虛函數(shù)表。而使用 CRTP,完全消除了動態(tài)綁定,降低了繼承帶來的虛函數(shù)表查詢開銷。
CRTP包含:
從模板類繼承,
使用派生類本身作為基類的模板參數(shù)。???????????????????????????????????????
?
這樣做的目的是在基類中使用派生類。從基礎對象的角度來看,派生對象本身就是對象,但是是向下轉(zhuǎn)換的對象。因此,基類可以通過將static_cast自身放入派生類來訪問派生類.
總結(jié)
為什么要掌握設計模式,軟件危機帶來剛性要求,設計模式提倡的高內(nèi)聚,低耦合,代碼復用,可擴展性等思想,可以給我們軟件設計帶來一些思考,有了思考,就會產(chǎn)生一些積極變化;
理解設計模式前提,是要理解背后的設計原則,這是整個設計模式的精華;
經(jīng)典的設計模式包含22種設計模式(沒有解釋器模式,日常開發(fā)中,很少使用),大致分為三類:創(chuàng)建型模式,結(jié)構(gòu)型模式,行為模式;
Linux系統(tǒng)里面包含大量設計模式思想,面向?qū)ο笤O計,List/Rbtree抽象設計,驅(qū)動框架bus總線解耦設計,都值得我們學習;
每種編程語言都會有一些獨特特殊習慣用法,Java的MVC,Golang的對象池模式(Object Pool)等,文中列舉的C++一些常見的慣用法RAII,Policy-based Design ,Pimpl,CRTP等,對C++開發(fā)來說,了解和掌握他們,對于特定場景問題多了一些好的解決方案;
設計模式是銀彈嗎?不是,就像軟件工程也不是銀彈一樣,這些都只是工具,關(guān)鍵還是看是否真正理解其背后反射出的設計精髓,我們需要多一些批判性的思考,沒有絕對好壞,軟件設計的最終方案很多時候都是權(quán)衡(trade-off)結(jié)果,但我們的長期目標始終沒有變化。
????????????
關(guān)注公眾號,回復"設計模式",打包送你設計模式經(jīng)典pdf資料和各種語言源碼。
大家好,我是Alex,希望你我都是一個勤勉的人,依靠自己的力量和毅力,從一堆紛繁復雜尋覓到了真正的知識,寫文章確實比較累,如果可以幫助到你,那也是值得的,希望大家多多點贊,在看,轉(zhuǎn)發(fā),你的舉手之勞都是我堅持寫作的動力,萬分感謝;
????????????????????本人工作多年,面試經(jīng)驗豐富(每年上百場面試),負責騰訊云網(wǎng)絡核心架構(gòu)設計和開發(fā),對大規(guī)模,大流量,高并發(fā),高可用,極致高性能系統(tǒng)有一定經(jīng)驗,想要更多技術(shù)討論,面試跳槽,技術(shù)咨詢,職場經(jīng)驗等可以關(guān)注公眾號和關(guān)注我,還可以進群和和一群技術(shù)愛好者討論技術(shù),聊聊天;????????????????????
?如有收獲,點個在看,誠摯感謝總結(jié)
以上是生活随笔為你收集整理的经典永不过时!重温设计模式的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Linux调度系统全景指南(终结篇)
- 下一篇: asp.net ajax控件工具集 Au