從面向對象的設計模式看軟件設計
前些天發了一篇《如此理解面向對象編程》的文章,然后引起了大家的熱議。然后我在微博上說了 一句——“那 23 個經典的設計模式和 OO 半毛錢關系沒有,只不過人家用 OO 來實現罷了……OO 的設計模式思想和 Unix 的設計思想基本沒什么差別”,結果引來了一點點爭議。所以,我寫下這篇文章把我的觀點說明一下。我希望這樣可以讓大家更容易地理解什么是設計模式。我隨便 幫 OO 和 Unix/Linux 搞搞基。
什么是模式
在正式說明 GoF 的那 23 個經典的設計模式其實和 OO 關系不大并和 Unix 的設計思想很相似的這個觀點之前,讓我先來說說什么是模式?設計模式的英文是 Design Pattern,模式是 Pattern 的漢譯。所謂 Pattern 就是一種規則,或是一種模型,或是一種習慣。Pattern 這個東西到處都是,并不只有技術圏子里才有。比如:
- 文章有文章的 Pattern。如新聞有新聞的 Pattern(第一段話簡述了整個新聞),詩歌總是抒情的,論文總是死板的,講稿總是高談的,漫畫總是幽默的,……
- 小說有小說的 Pattern。比如,
- 武俠小說必然要整個武林大會,整幾個 NB 的武功和大師,分個正派和反派,還有一個或數個驚天陰謀,壞人總是要在一開始占盡優勢,好人總是要力挽狂瀾……
- 言情小說總是要有第三者,總是要有負心人,里面的女子總是要哭得死去活來,但又癡心不改,……
- 新聞聯播的模式是:頭 10 分鐘領導很忙,中間 10 分鐘人民很幸福,后 10 分鐘國外很亂。中國政府官方宣傳稿也模式也很明顯,各種贊美,口號,勝利,總是要堅持個什么,團結個什么,邁向個什么,某某精神,某某思想,群眾情緒穩定,不明真相,等等……
- 春節的模式是,回家,吃餃子,放個鞭炮,給壓歲錢,同學聚會…… 同學聚會的模式基本上都是在飯桌上回憶一下校園時光,比較一下各自的當前處境,調戲一下女同學……
- …… …… </ul>
- 移動公司的合約機計劃,88 套餐(通話 100 分鐘,短信 100 條,彩信,20 條,上網 200M),128 套餐(通話 200 分鐘,短信 150 條,彩信 50 條,上網 500M)……
- 家里的裝修,總是要有廚衛,有門,有燈,有沙發,有茶幾,有床,有衣柜,有電視,有冰箱,有洗衣機……,這些都是必需的,只是每個家庭里的具體裝修不一樣。
- Diablo 游戲中的 Normal,Hard,Nightmare,Hell 模式,這些模式的怪和場景和故事情況都差不多,就是每個場景的怪物和裝備的屬性不一樣。或是 WarCraft 中的地圖就是一個 Abstract Factory 模式(注:Warcraft 的地圖什么都能干)。這和學校中的小學,初中,高中,大學差不多,都是一樣的學習環境,一樣的教學方式,一樣的教室,都要期中考和期末考,都有班長和科代 表,就是學的東西的難度不一樣,但基本上都是語文,英語,數,理,化,還有永遠都有的政治課。學校就是一個抽象工廠。 </ul>
這就是 Pattern,只要你細心觀察,你會發現這世間有很多很多的 Pattern。
GoF 的 23 個設計模式
《設計模式》這本書中,GoF 這四個人總結了 23 個經典的面向對象的設計模式,某中有 5 個創建模式,7 個結構模式,11 個行為模式。很多人都會覺得這是面向對象的設計模式,很多人也覺得非面向對象不能用這些模式。我覺得這是一種教條主義。就像《那些流行的編程方法》中的“設計模式驅動型編程”一樣,就像《如此理解面向對象》一樣的那么的滑稽。
好了,回到我的論點——“GoF 的這 23 個設計模式和 OO 關系不大,并且和 Unix 的設計思想基本一致,只不過 GoF 用 OO 實現了它們”,就像我上面說過的那些生活中的 Pattern 一樣,只要你仔細思考,你會發現這 23 個設計模式在我們的生活和社會中也能有他們的身影。而且也一樣可以用 OO 的方式實現之。
讓我們來看看這 23 個經典的設計模式中的幾個常用的模式:
Factory 模式,這個模式可能是是個人都知道的模式。這個模式在現實社會中就像各種工廠一樣,工廠跨 界的不多,基本上都是在生產同一類的產品,有的生產汽車,有的生產電視,有的生產衣服,有的生產衛生紙……基本上來說,一個生產線上只有做同一類的東西。 這和 Factory 模式很相似。編程中,像內存池,線程池,連接池等池化技術都是這個模式,當然,Factory 給你的一個對象,而不單單只是資源,factory 創建出來的對象都有同樣的接口可以被多態調用。這其實和 Unix 把所有的硬件都 factory 成文件一樣,并提供了 read/write 等文件操作來讓你操作任意設備的I/O。
Abstract Factory:抽象工廠這個模式是創建一組有同一主題的不同的類。這個模式在現實社會當中也有很多例子,比如:
這就是抽象工廠的業務模型(或是:Business Pattern),你覺得是不是不一定非要用 OO 來實現這樣的模式?(我們思考一下,我們會不會被先入為主了,覺得不會 OO 都不知道怎么實現了),不用 OO,用相同格式但內容不同的配置文件是不是也能實現?在 Unix 下,抽象工廠這個模式在 Unix 下就像是/etc/rcX.d 下的那些東西,1 代表命令行單用戶,2,代表命令行多用戶,3 代表命令行多用戶完整模式啟動,5 代表圖形界面啟動,0 代表關機,6 代表重啟,你要切換的話,init
Proxy 模式,原型模式,復制一個類的實現。這個模式在現實中的例子也有很多:傳真,復印,都是這個模式。Unix 進程和 Github 項目的 Fork 就是一種。進程 fork 明顯不是 OO 的模型(參看:關于 Fork 的一道面試題)。用非 OO 的方法同樣可以實現這個模式。
Singleton 模式,單例模式。生活中,公司只有一個 CEO,法律限制你只能有一個老婆,你只能有一個身份證號,一個 TCP 端口只能被一個進程使用,等等。軟件開發方面,并不一定只有 OO 才能做到,你可以用一個全局變量,一個中心服務器,甚至可以使用行政手段來約束開發中不會出現多個實例。Unix 下實現單例進程的一個最常用的實踐是在進程啟動的時候用“(S_IRUSR S_IWUSR S_IRGRP S_IROTH)”模式打開一個“鎖文件”。
Adapter 模式,適配器模式。可以兼容歐洲美國中國的插頭或插座,萬能讀卡器,可以播放各種格式多媒 體文件的插放器,可以解析 FTP/HTTP/HTTPS/等網絡協議的瀏覽器,可以兼容各大銀行的銀聯接口、支付寶、Paypal、VISA 等銀行接口,可以適配各種后端的解釋器的 Nginx 或 Apache,等等。用非 OO 的編程方式就是重新包裝成一個標準接口。這個模式很像 Unix 下的/dev 下的那些文件,操作系統把系統設備適配成文件,于是你就可以使用 read/write 來進行讀寫了。
Bridge 模式,橋接模式。這個模式用的更多,比如一個燈具可以接各種燈泡或燈管,一個電鉆可以換上不 同的鉆頭來適應不同的材料,一輛汽車可以隨時更換不同的輪胎來適應不同的路面,你的桌面可以隨時更換一個圖片來適應你的心情,你的單反相機可以更換不同的 鏡頭來拍不同的照片…… 橋接模式說白了就是組件化,模塊化,可以自由拼裝。在 OO 中,其主要是通過讓業務類組合一個標準接口來完成,這在非 OO 的程序設計中用得實在是太多了,主要是通過回調函數或是標準接口來實現。這個也是 Unix 設計哲學中的主要思想。在 Unix 中,文件的權限使用的就是 Bridge 模式,標準接口是用戶,用戶組和其它,rwx 三個模式,然后用 chmod/chown 改一改,這文件就有不同的屬主和屬性了。
Decorator 模式,裝飾模式。這個模式在生活中太多了,你給你的手機或電腦貼個什么,掛個什么,吃 東西的時候加點什么佐料,多點肉還是多個蛋,一個 Unix/Linux 命令的各種參數是對這個命令的修飾,等等。我覺得這個模式在 Unix 中最經常的體現就是通過管道把命令連接起來來完成一個功能,比如:ps -elf 是列進程的,用管道 grep hchen 就可以達到過濾的目的,grep 的邏輯沒有侵入 ps 中,grep 修飾了 ps,但是其組合起來完成了一個特定的功能。可見,這和 OO 沒有什么關系。
Facade 模式,這個模式我們每個人從會編程的時候就在無意識地用這個模式了。這個模式就是把一大堆類 拼裝起來,并統一往外提供提口。在現實生活中這樣的例子太多了,比如:旅行社把機票,酒店,景點,導游,司機,進店打了一個包叫旅行;IBM 把主機,存儲,OS,J2EE,DB,網絡,流程打了個包叫企業級解決方案。Unix 中最典型的一個例子就是用 Shell 腳本組合各種命令來創造一個新的功能,這是的 Shell 中的各種命令通過標準I/O這個接口進行組合交互。
Proxy 模式,代理模式。我們租個房,買個機票,打個官司,都少不了代理,人大代表代理了老百姓去行使 政治權力。我們去飯館里吃飯也是一種代理模式,因為我們只管吃就好了,洗菜做飯洗碗的工作都被 Proxy 幫你干了,于是你就省事多了。操作系統就是硬件的代理,CDN 就是網站的代理,……使用代理你可以讓事情變理更簡單,也可以在代理層加入一些權限檢查,這樣可以讓業務模塊更關注業務,而把一些非業務的事情剝離出來交 給代理以完成解耦。可見這個模式和 OO 沒啥關系。Unix 下這個模式最佳體現就是 Shell,它代理了系統調用并提供 UI。還有很多命令會幫你把/proc 目錄下的那些文件內容整理和顯示出來。
Chain of Responsibility 模式,劫匪來搶銀行,保安搞不定,就交給 110,110 搞不定就交給武警。有什么事件發生時的響應的 Escalation Path,辦公中的逐級審批。這個模式用一個函數指針數組或是棧結構就可以實現了。這個思想很像編程中的異常處理機制,一層一層地往上傳遞異常直到異常被 捕捉。在 Unix 下,一個最簡單的例子就是用 && 來把命令拼起來,如:cmd1 && cmd2 &&, 如果 cmd1 失改了,cmd2 就會執行,如果 cmd1 和 cmd2 都失敗了,cmd3 才會執行。如: cd lib && rm -rf .o 或 ping -c1 coolshell.cn && ssh haoel@coolshell.cn
Command 模式,這恐怕是軟件里最多的模式了,比如:編譯器里的 Undo/Redo,宏錄制。還有數據庫的事務處理,線程池,設置向導,包括程序并行執行的指令集等等。這個模式主要是把一個對象的行為封裝成一個一個的 有相同接口的 command,然后交給一個統一的命令執行器執行或管理這些命令。這個模式和我們的 Unix/Linux 機器啟動時在/etc/init.d 下的那些S和K開頭的腳本很像,把各種 daemon 的啟動和退出行為封裝成一個腳本其支持 reload/start/stop/status 這樣的命令,然后把他們按一定的規范做符號鏈接到/etc/init.d 目錄下,這樣操作系統就會接管這些 daemon 的啟動和退出。
Observer 模式,觀察者模式,這個模式也叫 pub-sub 模式,很像我們用手機訂閱手機報,微博的 follow 的信息流也是這樣的一個模式。MVC 中的C會 sub V 中的事件,用非 OO 的方式其實也是一個回調函數的事。在很多異步系統中,你需要知道最終的調用有沒有成功,比如說調用支付寶的支付接口,你需要向支付寶注冊一個回調的接口, 以便支付寶回調你。Linux 下的一些系統調用如 epoll/aio/inotify/signal 都是這種思路。
Strategy 模式,策略模式,這個模式和 Bridge 模式很像,只不過 Bridge 是結構模式,其主要是用于對象的構造;而 Strategy 是行為模式,主要是用于對象的行為。策略模式很像瀏覽器里的各種插件,只要你裝了某個插件,你就有某個功能。你可以安裝多個插件來讓你的瀏覽器有更多的功 能(書本上的這個模式是你只能選用一個算法,當然,我們不用那么教條)。就像《你可能不知道的 Shell》中的那個設置設置$EDITOR 變量后可以按 ctrl+x e 啟動編譯器,或是用 set -o vi 或 set -o emacs 來讓自己的 shell 像 vi 或 emacs 一樣,或是像 find -exec 或 xargs 一樣的拼裝命令。
Bridge 和 Strategy 是 OO 設計模式里的“Favor Composition Over Inheritance” 的典范,其實現了接口與實現分離的。Unix 中的 Shell 就是一種,你可隨意地更換不同的 Shell。還有 Emacs 中的 LISP 驅動C,C實現了引擎,交給 LISP 實現邏輯。把程序分為前端和后端,通過 socket 專用應用協議進行通訊,前端實現策略,后端實現機制。再看看 makefile 把編譯器和源代碼的解耦,命令行輸出這個接口可以把一個復雜的功能解耦并抽像成各種各樣小而美的小功能命令,等等這樣的例子,你會發現,還有大量的編程框 架都會多少采用這樣的思想,可以讓你的軟件像更換汽車零件一樣方便。我在用 Unix 的設計思想來應對變更的需求中說過燈具廠,燈泡廠,和開關廠的例子。
后記
因為寫作倉促,上面的那些東西,可能會你讓你覺得有些牽強,那么抱歉了,你可以幫我看看在生活中和 Unix 里有沒有更帥的例子。
不過,我們會發現上面 OO 搞出來的那么多模式在 Unix 下看來好像沒有那么復雜,而且 Unix 下看起來并沒有那么多模式,而且 Unix 中的設計模式無非就是這么幾個關鍵詞:單一,簡潔,模塊,拼裝。我們再來看看 OO 設計的兩大準則:1)鐘情于組合而不是繼承,2)依賴于接口而不是實現。還有S.O.L.I.D 原則也一樣(如果你仔細觀察,你會發現 SOLID 原則在 Unix 下也是完美地體美)。你看,Unix 和 OO 設計模式是不是完美的統一嗎?
我有種強烈的感覺——Unix 對這些所謂的 OO 的設計模式實現得更好。因為 Unix 就一條設計模式!再次推薦《The Art of Unix Programming》
餐后甜點
我上面提到了《The Art of Unix Programming》,所以我有必要再談談這本書中我中毒最深的一章《模塊性:保持清晰和簡潔》中所談到的膠合層。
膠合層這一節中說了,我們開發軟件一般要么 Top-Down,要么 Bottom-Up,這兩種方法者有好有不好。頂層一般是應用邏輯層,底層一般是原語層(我理解為技術沉淀層,或是技術基礎層)。自頂向下的開發,你可能 會因為開發到底層后發現底層可沉淀的東西越來越不爽(因為被可能被很多業務邏輯所侵入),如果自底向上的開發,你可能越到上層你越發現很多你下面干的基礎 上工作有很多用不上。所以,最好的方式是同時進行,一會頂層,一會底層,來來回回的開發——說白了就是在開發中不斷的重構,邊開發邊理解邊沉淀。
無論怎么樣,你會發現其中需要一層膠合層來膠合業務邏輯層和底層原語層(軟件開發中的業務層和技術層的膠合),Unix 的設計哲學認為,這層膠合層應該盡量地薄,膠合層越多,我們就只有可能在其中苦苦掙扎。
其實,膠合層原則就是分離原則上更為上層地體現,策略(業務邏輯)和機制(基礎技術或原語)的清楚的分離。你可以看到,OO 和 Unix 都是在做這樣的分離。但是需要注意到的時,OO 用抽象接口來做這個分離——很多 OO 的模式中,抽象層太多了,導致膠合層太過于復雜了,所以,OO 鼓勵了——“厚重地膠合和復雜層次”,反而增加了程序的復雜度(這種情況在惡化中)。而 Unix 采用的是薄的膠合層,薄地相當的優雅。(通過這段話的描述,我相信你會明白了《如此理解面向對象編程》中的個例子——為什么用 OO 來實現會比用非 OO 來實現更為地惡心——那就是因為 OO 膠合層太復雜了)
OO 的最大的問題就——接口復雜度太高!(注:Unix 編程藝術這本書里說了軟件有三個復雜度:代碼量、接口、實現,這三個東西構成了我們的軟件復雜度)
再送一個果盤
大家一定記得《SteveY 對 Amazon 和 Google 平臺的長篇大論》 中 Amazon 中那個令人非常向往的 SOA 式的架構。因為以前在 Amazon,有些話不好說。現在可以說了,我在 Amazon 里,我個人對這個服務化的架構相當的不待見,太復雜,復雜以亂七八糟,方向是好的,想法也是好的,但是這東西和 OO 一樣,造成大量的接口復雜度,今天的 Amazon,完全沒人知道各個服務是怎么個調用的,一團亂麻(其內部并不像你看到的 AWS 那么的美妙。注:AWS 是非常不錯的,是相當好的設計)。
那么我們怎么來解決 SOA 的接口復雜度問題?其實,Unix 早就給出了答案——數據驅動編程(詳見: 《Unix 編程藝術》的第 9.1 章),在我離開 Amazon 的時候,美國總部的 Principle SDE 們在吐槽今天 Amazon 的 SOA 架構,更好的架構應該是數據驅動式的。(今天還在 Amazon 的同學可以上內網 boardcast 上看看相關的 Principle Talk 視頻)
(注:這本來是我想在 2012 年杭州 QCon 上的分享的一個主題,無奈當時被大會組織者給拒了,所以只好講了一個《建一支小團隊》,今天有多人還是不能明白甚至反感我的那個《小團隊》的演講,但是我 相信那是必然的趨勢,就像十年前大家在說“程序員只能干到 30 歲”時,當時的我我卻毫不猶豫地相信十年后,30 歲以上的有經驗的老程序員一定會成為各個公司角逐和竟爭的紅人)