醒醒吧,設計模式沒用的

設計模式是軟件工程中聽起來非常深奧,也非常高端的一個詞彙,似乎有了設計模式,我們的代碼和項目就能自然的變得非常合理並且易於擴展和維護,然而事情並沒有這麼簡單,軟件工程中沒有銀彈。

我們在今天談論設計模式時,往往與 1994 年 Erich Gamma, John Vlissides, Richard Helm, Ralph Johnson 四個人出版的《設計模式》一書有著密切的關係,想要避開這本書討論設計模式也非常困難。這本書在出版的 6 年後被機械工業出版社於 2000 年引進於中國,剛被引進的時候,因為大環境的原因,這本書還不是特別火熱,但是自從 2014 年 5 月開始,由於國內互聯網行業的迅速發展和信息產業的進步,設計模式一詞也變得越來越熱門。

醒醒吧,设计模式没用的

圖 1 - 設計模式在中國的搜索指數

很多工程師都在研究設計模式,企圖讓自己設計的軟件變得更加優秀,一些候選人也在簡歷上寫著自己掌握多少種設計模式並在面試時討論書中的那 23 種設計模式。與設計模式在中國的流行相比,在全世界範圍內,設計模式的熱度從 2004 年開始卻一直在下降。

醒醒吧,设计模式没用的

圖 2 - 設計模式在全球的搜索指數

上述數據難道說明全世界的開發者都不在乎如何設計更優秀的軟件嗎,作者覺得這答案一定是否定的,設計出架構良好的軟件是工程師追求的目標,設計糟糕的軟件是無法持續維護的,只能一次又一次的重構或者重寫。為了減少自己的麻煩,雖然從短期看來工程師都傾向於寫出容易實現的邏輯,但是從長期看來工程師更傾向於寫出容易維護的代碼。

作者在本文的觀點是,學習設計模式與設計優秀的軟件並不相關,盲目追求和套用書中的設計模式只能使項目變得更加糟糕,本文並不是要批判《設計模式》中提出的通用的設計模型,我們需要對書中的內容多一些批判性的思考,想清楚究竟什麼樣的設計才是能夠保留下來的。

需要注意的是,這篇文章充滿了作者的主觀意見和個人經驗,如果讀者不認同本文的觀點,歡迎在文章下面留言並討論。

在討論設計模式的作用之前,我們需要先理解它的定義。在軟件工程中,軟件設計模式是在特定上下文下對於普遍出現問題的可重用解決方案。這個定義非常嚴謹,我們可以發現幾個關鍵的形容詞,特定的(Specific)、普遍的(Commonly)和可重用(Reusable),這幾個形容詞說明了設計模式的特性以及它的侷限性,本文將分三個部分分析作者為什麼覺得設計模式沒用。

可重用的解決方案

抽象的設計模式是從不同具體項目中總結出來的通用經驗,從具體到抽象的過程相對容易,然而從抽象的模式套用到具體場景卻很困難,如果沒有足夠的經驗或者思考只會做出拙劣的設計。設計模式是從具體場景中總結的解決方案,它會站在一個更高的角度提煉實現中的關鍵細節,這樣才能提供更好的通用性。

醒醒吧,设计模式没用的

圖 3 - 抽象的模式與具體的實現

因為我們需要在多個不同的實現和場景中尋找類似的模式,所以在提煉設計模式的過程中一定會失去很多實現細節。作為理論來講,精煉的、抽象的定義才能夠更好的傳播和重用,但是不同的讀者在理解這些定義時會遇到兩個問題:

  • 經驗較少的工程師 - 雖然書中的例子都看懂了,但是一到具體場景就沒有辦法利用;

  • 有經驗的工程師 - 雖然書中的定義和例子都沒有問題,但是這些我們早就知道了。

相信稍微有一些編程經驗的人都能從經歷過的項目中總結出很多模式,從具體到抽象的過程需要積累較多的素材,不過這個過程相對比較容易 — 當我們學習的、實踐的項目足夠多時,我們自然能夠發現其中存在的常見模式。

如果要從抽象的理論直接推導出出具體的實現是比較困難的,我們在這裡簡單舉一個例子,我們都知道增加副本和備用服務器是提高服務可用性的一種常見方式 —

避免單點故障可以提高系統的可用性。當這種看起來普通的大白話出現在書時,我們往往都會一帶而過,覺得這是廢話。然而在實踐中,我們可以遵循這個理論設計出從簡單到複雜的服務架構,例如:同機房的多實例部署和異地多活等方式。

醒醒吧,设计模式没用的

圖 4 - 異地多活容災

多實例部署和異地多活都是能夠避免單點故障的方法,然而實現這些方案需要考慮非常多的細節,這些細節都是在理論中缺失的,是需要通過經驗和思考來補齊的,例如:部署多個實例之後,我們是不是還需要考慮服務註冊、服務發現以及負載均衡的路由策略;異地多活是不是也要考慮機房之間的網絡延遲、專用網絡通道的搭建以及數據不一致的問題,這些實現細節在總結成規律抽象的理論時基本都消失了。

The devil is in the detail.[^5] - Ludwig Mies Van Der Rohe

而且並不是居高臨下的架構設計才是系統設計,每個包、方法甚至代碼中的空行中都體現了作者的設計思路,抽象的理論和模式能夠起到指導的作用,但是真正讓設計融入系統的還是工程師的豐富經驗和深入思考。

錘子和釘子

設計模式既不能讓我們學到絕世武功,也不能幫我們打通任督二脈,從此看破系統中的所有巧妙設計,認為自己學到了神功,想要在項目中大展身手套用書中的設計時很有可能會帶來錯誤設計,成為項目中的遺留代碼(Legacy code)並被接手的工程師吐槽和重構。

如果我們手中只有 23 種設計模式能夠指導系統的設計,那麼待解決的軟件設計問題看起來都像釘子一樣,看起來都可以用這些模式『巧妙』的解決,但是場景與場景是非常不同的,我們要清楚地知道設計模式的侷限性以及待解決問題的上下文,才能做出好的設計。

If all you have is a hammer, everything looks like a nail! [^6] - Charlie Munger

《設計模式》書中描述的 23 種模式是有相當侷限性的,如果仔細閱讀這本書的副標題可能會讓我們在應用具體的設計模式之前變得更加謹慎 — "Elements of reusable Object-Oriented Software.",這個副標題非常清晰的介紹了書中涉及的內容。

我們今天熟悉的大多數編程語言都是在上世紀 80 ~ 90 年代誕生的,包括 C++、Python、Ruby、Java、PHP 和 JavaScript,大多數的語言都是面嚮對象語言,而書中介紹的設計模式也都是在使用面嚮對象語言的項目中總結的。

醒醒吧,设计模式没用的

圖 5 - 編程語言的誕生時間

今天的編程語言已經變得越來越複雜,多數語言都同時支持多種編程範式 — 面向對象、函數式編程等,在應用書中的設計模式時,我們需要充分考慮到其解決的關鍵問題以及我們手中的編程語言是否有更好的實現方式,本節將通過以下兩部分內容分析設計模式的侷限性:

  • 不同面向對象編程語言使用不同方式支持三大特性 — 封裝、繼承和多態;

  • 多範式逐漸成為現代編程語言的主流,面向對象中複雜的設計可以被其他編程範式簡化。

面向對象與面向對象

雖然《設計模式》中使用 Smalltalk 和 C++ 介紹一些場景,但是在我們實踐中使用的面嚮對象語言可能具有不同的特性,所以同一模式會有不同的實現方案,這些方案是我們在書中完全學不到的,如果我們套用固定的模式,最終一定會得到詭異的結果。

醒醒吧,设计模式没用的

圖 6 - 觀察者模式結構[^7]

我們以上圖的觀察者模式為例,所有的面向對象編程語言都可以實現上述複雜的結構,如果我們套用書中的模式確實可以得到一個標準的觀察者模式,但是一些編程語言提供了一些更簡潔的設計,iOS 平臺上的 Objective-C 語言就使用鍵值觀測機制(Key-Value Observing)實現觀察者模式,它的實現與書中提供的觀察者模式完全不同:

// 註冊觀察者

[self.object addObserver:self

forKeyPath:@"age"

options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld

context:];

// 獲得回調

- (void)observeValueForKeyPath:(NSString *)keyPath

ofObject:(id)object

change:(NSDictionary *)change

context:(void *)context {

// ...

}

不僅實現模式的方式不同會導致我們在運用時會遇到諸多問題,不同面嚮對象語言對封裝、抽象和多態的支持也不同。面向對象是一個非常廣泛的概念,只要我們的編程範式是基於對象和方法的,就可以被認為支持面向對象的編程範式。

醒醒吧,设计模式没用的

圖 7 - 面向對象的特性

不同的編程語言可能選擇支持多重繼承、單繼承或者組合,也可能使用不同的方式實現接口,Java8 的接口方法可以定義默認實現,而 Objective-C 中的接口(Protocol)只是一組待實現的方法簽名,這些語言特性帶來的差異是需要我們在研究工具時不斷摸索的。

面向對象與多編程範式

21 世紀誕生的一些編程語言與過去的編程語言有著很大的不同,不僅新的編程語言開始接收函數式編程中的一些思想和設計,上個世紀誕生的編程語言也在吸納不同的編程範式,函數和方法成為了語言中的一等公民,我們可以直接向函數中傳遞函數來簡化過去複雜的類關係。

我們在這裡還是以觀察者模式為例,在純粹的面嚮對象語言中,因為函數不能作為參數傳入另一個函數,所以我們需要構造一個新的類 Observer,讓這個類實現 Update 方法,再將這個類的實例傳入方法,當事件發生時就可以通過類上附著的方法傳遞消息。將函數包裝到類中、初始化實例並傳入函數的過程聽起來都非常繁瑣,而函數式編程可以簡化這個問題:

醒醒吧,设计模式没用的

圖 8 - 函數式編程範式

在多編程範式的編程語言中,我們雖然可以使用這種傳統的、囉嗦的方式實現觀察者,但是直接傳遞函數(Procedure)是一種更加高效的做法,為我們省去了很多冗餘的中間抽象,能夠讓程序的設計變得更加簡潔:

object.OnUpdate(func(u *updates) {

...

})

一些讀者可能會認為上述的這種方式與面向對象中的觀察者模式沒有本質的不同,這句話說的沒有問題,因為從理論上來講,幾乎所有的通用編程語言都是圖靈完備的,我們可以用一門語言實現另一個語言的編譯器或者解釋器,進而支持其他語言的特性並通過其他語言的語法來編寫代碼,然而這種本質論在這個討論下沒有太多意義。

不同的觀察者模式雖然本質上是一樣的,但是因為工具的區別,不同語言的實現也是不同的,簡單和清晰的實現往往都會取代複雜的實現,多範式的編程語言可以使用不同的特性來簡化設計並減少不必要的抽象。

通用的術語

設計模式作為通用的術語確實可以增加不同工程師之間的溝通效率,但是降低溝通成本的前提是雙方對同術語有著相同的並且正確的認識,如果雙方的理解有差異,反而會製造更多的困惑。我們可以將 23 種不同的設計模式分成兩部分來分析,其中一部分是單例模式、抽象工廠模式這些被廣泛接受並理解的模式,另一部分是迭代子模型、命令模式和解釋器模式等不容易被理解的複雜模式。

從單例模式以及觀察者模式的命名,我們就能猜到它們想要解決的問題,使用類似的術語也很難造成歧義,確實能夠起到提高溝通效率的作用。不過,對於複雜的設計模式想要正確理解就非常困難,更不用說用來溝通了。

醒醒吧,设计模式没用的

圖 9 - 設計模式與術語

這裡只是一個比較簡單的劃分,不同人對模式難易的認知有比較大的差異,這裡的核心問題是書中列舉的 23 種設計模式真的是被廣泛傳播和認可的麼。在作者看來,設計模式雖然流傳的很廣泛,但是並不是所有人都清楚書中每一個模式的定義和場景,這也就喪失了提高溝通效率的作用,而常見的模式也不需要通過閱讀《設計模式》來學習。

假設溝通的雙方都瞭解不同的設計模式,那麼用設計模式來描述系統中的設計確實能夠提升溝通效率,讓信息的接收方能夠快速對設計有一個總體的概念,這能夠幫助我們快速理解設計的框架,但是我們仍然需要補全對細節的認識,就像我們在前面提到的 — 細節在設計中非常重要。

總結

軟件系統中處處都是設計,學習設計模式無法讓我們成為優秀的工程師,如果我們錯誤的理解了這本書的目的,以為自己學到了軟件設計或者面向對象設計的精髓,那就大錯特錯了。軟件設計的能力並不是一朝一夕就能培養出來的,它需要我們不斷對代碼進行思考,理解可能存在的擴展點並設計合理的抽象。

作者認為《設計模式》一書的定位比較尷尬,沒有經驗的工程師閱讀設計模式一定會覺得雲裡霧裡,書中講的東西都是廢話;經驗較少的工程師會嘗試套用書中的模式,設計出一些糟糕的代碼,反而誤以為自己的設計恰到好處;而經驗豐富的工程師在閱讀這本書時會覺得沒有什麼作用,只是為過去的設計找到了比較合適的名字。如果有人希望作者能夠推薦一些值得閱讀的書籍,《設計模式》一定不在這個書單上。

我們在這裡重新總結一下本文主張的觀點以及支撐幾個觀點的相關論據,學習設計模式與設計優秀的軟件並不相關,盲目追求和套用書中的設計模式只能使項目變得更加糟糕:

1)設計模式都是從很多項目中總結出的通用解決方案,從具體的實現中總結出抽象的模式相對比較簡單,但是想要將抽象的模式套用到場景中卻非常困難;

2)設計模式的出現本身就處在一個比較特殊的背景下,今天的編程語言大多采用了多種編程範式,很多在面向對象中需要用模式來解決問題已經被簡化;哪怕是對於面嚮對象語言,不同語言也使用不同方法實現同一特性,我們需要注意該書的副標題以及它的侷限性;

3)設計模式成為通用術語的前提是溝通的雙方對術語有著相同的認識和理解,但是很多複雜和偏門的設計模式並不會被人們熟知,單例等常見模式也不需要通過書本專門學習。

通過看書來學習軟件設計幾乎是一個不可能完成的任務,作者認為學習如何為程序編寫單元測試對學習系統設計極其重要

,提升項目單元測試覆蓋率的過程會讓我們思考如何寫出更利於測試的代碼,雖然軟件工程中沒有銀彈,但是單元測試不是銀彈可能也所差無幾了。

Design is the art of arranging code to work today, and be changeable forever. --Sandi Metz

我們在學習設計的過程中如果不看《設計模式》這本書可能會少走一些彎路,理解了一些抽象出來的定式可能會限制我們的思考,想要學習軟件設計就要去真正優秀的開源項目學習頂層設計到底層實現並在項目中不斷實踐,在最後我們會發現系統中到處都是設計和模式,而軟件開發中沒有聖盃。

每個架構師都夢想架構世界,設計未來,不妨來Gdevops全球敏捷運維峰會北京站看看業內流行架構趨勢吧:

  • 《銀行數字化轉型戰略分析、關鍵技術及未來架構趨勢》建信金科 資深業務架構師/《企業級業務架構作者》/《銀行數字化轉型》作者 付曉巖

  • 《建設敏捷型消費金融中臺及雲原生下的DevOps實踐》

    中郵消費金融 總經理助理 李遠鑫

  • 《平安銀行“傳統+互聯網”混合CMDB及運營中臺實踐》平安銀行 運營開發負責人 徐大蔚

  • 《中國農業銀行信貸中臺及數據中臺建設實踐》中國農業銀行 研發中心資深架構專家 張亮

  • 《技術體系建設:架構、質量、中臺、後端的戰略落地與矛盾破解》58到家集團/快狗打車 技術VP/CTO 沈劍

  • 《微服務技術體系落地:分佈式調用鏈追蹤系統實戰》58到家集團/快狗打車 技術委員會成員/架構部負責人 王昊

  • 《揭露現象看本質:到家、貨運O2O業務試金石基礎-中臺建設》

    58到家集團/快狗打車 業務服務部負責人/中臺技術部負責人 李洪英

9月11日,讓我們一起用更多元的視角觀察問題本質,尋找架構發展的軌跡和趨勢。

醒醒吧,设计模式没用的


分享到:


相關文章: