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