讀《圖解設計模式》的所思所想

智聯大前端發表於2019-08-05

困惑

作者試圖從另一個角度闡述設計模式,所以對 23 種具體設計模式進行了重新分類,但整本書讀下來比較困惑,在於幾點:

  1. 分類標準不統一,有實現思路、實現內容、模式目的等標準,甚至還有“適應設計模式”這種分類,頗有些無從分類的“自暴自棄”的味道。同時在這種分類方式下,還存在一個問題,即某設計模式的實現是會用到另一個設計模式的,甚至其些設計模式的書中實現類圖會基本相同,但是卻屬於不同分類,帶來了新的困惑,好像要強迫你在學習一個設計模式時,要忘掉其他設計模式的存在。
  2. 缺乏對具體設計模式適用場景的充分闡述,知何卻不知為何。
  3. 作為入門書,未對更低層的原則進行科普,即使知道了各具體模式可以達成哪些具體目的,卻無法融匯到統一的思想出口。
  4. ?

但是總覺得還有一個抓不到的原因,那麼再深入探究一下,到底是什麼令我產生困惑呢?這就需要了解設計模式的起源。

根源

設計模式(design pattern)是一套被反覆使用、多數人知曉的、經過分類編目的、程式碼設計經驗的總結。它描述了在軟體設計過程中的一些不斷重複發生的問題,以及該問題的解決方案。

設計模式是問題的方案。

設計模式是經驗的總結。

首先,正確的學習方式應該是帶著問題找答案。如果答案被直接擺在面前卻不知問題,不論是誰都會產生“這是啥?!”的困惑。但更重要的是,經驗是具有普適性的,具體的設計模式其實體現的是具體的思想方式,這種思想方式與語言無關,同時在單一語言中也一定有多種實現形式,那麼此時就進入到了抽象和具象的衝突。若無頓悟的天分,接收到思想的抽象概念描述時,會有一種腦子懂了,卻無從下手的感覺,同時若以書中有限的應用示例來描述,又無法完全體會到思想的方方面面。

所以設計模式的學習應該是快速的閱讀書籍,在對模式有輪廓性認識後,帶著問題,不斷實踐練習的一個過程,要在實踐中得出自己的體會,將從書中得到的融到自己的骨子裡。這也是造成前面講的困惑的根本原因了,實踐不夠呀。

這其中還有另一個教訓,我曾經陷入了為什麼能用這種設計模式而不能用另一種設計模式的思維旋渦,一樣,只靠想,不依託實踐,這些問題是解決不了的。所以不要把時間浪費在糾結的思考中。

也是因此,後續內容不會是面面俱到的長篇累牘,只會對設計模式的脈絡做基於目的的簡要闡述。

目的與手段

維護一個軟體的長期良性發展是究極目標,即提高可維護性,降低維護成本。可以從抽象等級分為 4 個層次。

  1. 目標:維護性。
  2. 標準:擴充套件性、重用性、高內聚、低耦合。
  3. 原則:7 大基本原則。
  4. 模式:23 + N 種設計模式。

應該通過提升擴充套件性、重用性等達到高內聚、低耦合的特性。

在這個過程中應遵循 7 大原則,同時這些原則又是設計模式的基礎,是設計模式為何如此設計的依據。

而模式則是更具體的思想正規化,設計模式不僅僅侷限於 23 種,跟隨技術水平的發展,也伴生出了新的問題,也就總結出了針對新問題的 N 種模式。

7 大基本原則

設計模式往往是基於類,介面來講的,而 JS 並非基於類的語言,支援度不夠,同時我們又不應該將模式的思想拘泥於類中,所以可以將下述原則的應用個體,如類、介面,放到函式或模組等其他維度上體會。

  • 單一職責原則:

    • 單一職責原則規定一個類應該有且僅有一個引起它變化的原因,否則類應該被拆分。
    • 我們不必要拘泥於類,該原則的根本目的是控制職責所在的個體複雜度。只需要明白單一個體只需要做好一件事,個體越簡單則可讀性越好,職責劃分越明確,則改動發生時,越不會影響其他個體。
    • 比如這種職責拆分可以發生在函式粒度,也可以發生在函式的聚合層面(類或者更外層的函式),職責和個體理想狀態下應該是一對一的。
    • 這個原則要求我們能清晰的認識到程式碼邏輯中的多重職責,從而才能進行劃分。
  • 介面隔離原則:

    • 客戶端不應該被迫依賴於它不使用的方法。一個類對另一個類的依賴應該建立在最小的介面上。
    • 即對於依賴者,被依賴者應該只提供他關心的功能。當體現在介面上時,就是介面隔離原則,將有冗餘的介面拆分。
    • 可以避免由於依賴者的增多導致介面膨脹,影響到其他的依賴者。
    • 相對於單一職責原則可以理解為單一職責原則是對內做最少承諾,而介面隔離原則是對外做最少的承諾。
  • 依賴倒置原則:

    • 高層模組不應該依賴低層模組,兩者都應該依賴其抽象;抽象不應該依賴細節,細節應該依賴抽象。
    • 即面向介面程式設計。我們只需要對低層進行介面定義,高層只需要關注有哪些介面並進行呼叫,低層實現時只要實現了這些介面,那麼傳給高層的例項發生變化時,高層就不需要修改。降低了耦合度。
  • 開閉原則:

    • 軟體實體應當對擴充套件開放,對修改關閉。
    • 在軟體修改時,儘量通過擴充套件而實現而不是通過修改來實現,避免對現有邏輯的影響。
  • 合成複用原則:

    • 在複用時,要儘量先使用組合(例項化是就存在)或者聚合(通過 API 呼叫新增為成員變數)等關聯關係來實現,其次才考慮使用繼承關係來實現。
    • 繼承強耦合,組合聚合是弱耦合。
  • 里氏替換原則:

    • 繼承必須確保超類所擁有的性質在子類中仍然成立。即子類可以擴充套件父類的功能,但不能改變父類原有的功能。
  • 迪米特法則:

    • 只與你的直接朋友交談,不跟“陌生人”說話,又叫最少知識原則。即一個類對自己依賴的類知道的越少越好。
    • 耦合是無法完全避免的。
    • 被依賴的類不論多複雜,都應該將細節封裝在內部,對外暴露 API。
    • 應該避免類中出現非直接的朋友關係(直接朋友關係:成員變數、引數、返回值)的依賴。

可以看出這些原則都是為了個體間的低耦合而努力。

模式的一句話描述

  1. 迭代器模式:為了在不暴露資料的內部結構的前提下對外提供可替換的迭代方式。此模式隱藏內部細節,且可替換迭代方式。這種思路可推廣至迭代以外的其他能力。
  2. 介面卡模式:為了使不相容的介面協同工作,將現有介面包裝為需要的介面。在處理程式碼邊界即第三方依賴時也可使用,能在第三方依賴被換掉時降低改動成本。
  3. 模板方法模式:在流程結構確定,而步驟的具體實現不定或有差異時使用。即定義好模板,但將具體處理的實現交給子類,擴充套件新的能力時只需實現新的子類。
  4. 工廠方法模式:建立介面不變的情況下,由使用者決定什麼哪個例項時,使用。產品和工廠一一對應,擴充套件新的產品時需要增加新的產品類和對應的工廠類。
  5. 單例模式:單一類只允許生成一個例項時使用。
  6. 原型模式:避免較高的例項化成本時使用。通過複製生成例項,核心在於複製現有例項,避免重走例項化的過程。
  7. 建造者模式:最終產出物的組成部分相同,但需要組裝過程可替換時使用。通過組裝生成複雜例項,並將組裝過程抽離至獨立的類,核心在於側重組裝,那麼實現不同的組裝過程類就能產出不同的產出物。
  8. 抽象工廠模式:與工廠方法模式類似,當工廠類產出多個產品時可以使用抽象工廠模式。注意區別是是工廠產出一個還是多個產品。
  9. 橋接模式:在對外提供的功能介面內有多個維度的變化時使用,將類的對外介面和實現分為獨立的兩個類,對外介面通過在內部組合使用實現類來完成具體實現,可減少維度引起的類數量的爆炸增長。
  10. 策略模式:當我們完成任務的策略需要可被替換時使用。將通過策略完成任務的過程拆分為呼叫和實現,實現部分提供成系統的方法簇,即具體策略,以引數形式傳給呼叫部分,從而實現策略可替換。
  11. 組合模式:當需要提供給使用者的是多個物件,且物件間是部分-整體的層次結構,且不希望使用者關心物件間差異,只要一套訪問介面時使用。通過多個子類實現相同介面實現。
  12. 裝飾器模式:給一個物件追加更多功能,且不改變提供給使用者的介面時使用。
  13. 訪問者模式:在不改變資料結構的前提下可以新增對資料結構的新的操作時使用。通過將資料結構與操作分離的方式實現。
  14. 責任鏈模式:個體需要被多個物件處理,但處理物件間有沒有耦合關係時,為了避免增加系統複雜度時使用。通過將多個處理物件組成一條責任鏈,然後將待處理目標沿著這條鏈傳遞進行處理實現。Koa 中介軟體機制就是一種實踐。
  15. 門面模式:簡化使用者對複雜系統中子系統的聯絡,對外提供簡單易用的介面時使用。通過包裝更高層的類,由它排程子系統實現。
  16. 中介者模式:簡化複雜系統中子系統之間的聯絡,將互動封裝一箇中介物件,降低子系統物件間的耦合。可以體會下與門面模式的不同。
  17. 觀察者模式:一個物件改變時需要導致其他物件也改變,且不關心其他物件具體是誰時可以使用。通過觀察物件管理監聽他的所有觀察者,並在發生變化時通知所有觀察者實現。
  18. 備忘錄模式:在不破壞封裝性的前提下,捕獲一個物件的內部狀態,並在該物件之外儲存這個狀態,以便以後當需要時能將該物件恢復到原先儲存的狀態時使用。
  19. 狀態模式:物件與外部互動導致狀態變化,從而行為不同時使用。通過將不同狀態下的行為封裝為不同的類,允許在狀態改變時通過切換狀態類改變行為實現。可以理解為策略模式的一種特殊應用。是一種內建了多種“策略”,根據狀態變化切換“策略”的模式。
  20. 享元模式:有大量的例項需要公用或重複使用時使用。我們可以把這些例項當做享元,並管理起來。可以當做同時使用了多個單例模式的類,因為存在管理能力,所以會受外部因素影響,在返回前修改單例的狀態。
  21. 代理模式:在我們不想讓使用者直接使用物件的情況下使用,如加以訪問控制。很簡單,實現方式就是加一層代理來間接引用物件。
  22. 命令模式:需要使命令發起者和執行者不可見,甚至需要對命令加以管理時使用。此模式通過將命令封裝為一個類,將命令執行者作為命令的依賴,分離命令呼叫者和命令實現者,同時由於命令例項的存在又可以對命令加以管理。
  23. 直譯器模式:將發生頻率足夠高的問題的各個例項表述為一個簡單語言的句子,並構建一個直譯器解釋語言中的句子。通過對定義語句語法節點,並針對每類語法節點宣告類,對語句節點遍歷解析實現。

我們可以從上述描述中看到重複的幾個關鍵詞:拆分、不關心、不破壞,還是在為了個體間的低耦合而努力。

而且要注意的是,模式並非完美,有些模式實現時甚至會增加內部耦合,增加系統複雜度,所以要關注目的,關注目的,關注目的,關注是否降低了所關注的可能變化的點的耦合度。

結語

最後以三個問題結束這篇文章。

學什麼?

我們學設計模式,是為了學習如何合理的組織我們的程式碼,如何解耦,如何真正的達到對修改封閉對擴充套件開放的效果,而不是去背誦那些類的繼承模式,然後自己記不住,回過頭來就罵設計模式把你的程式碼搞複雜了,要反設計模式。

如何用?

為了合理的利用設計模式,我們應該明白一個概念,叫做擴充套件點。擴充套件點不是天生就有的,而是設計出來的。我們設計一個軟體的架構的時候,我們也要同時設計一下哪些地方以後可以改,哪些地方以後不能改。

如何用的好?

“我亦無他,惟手熟爾。”

參考

相關文章