前端和設計模式
年前和朋友聊天,說起了程式設計原則,我說程式設計的原則就是「高內聚,低耦合」吧。
朋友說,「不是,程式設計的原則有六個。」
我說,「對啊,高、內、聚、低、耦、合,不就是六個麼?」
朋友一聲冷笑,「你們前端果然都不懂程式設計,麻煩你繼續往下編。」
我說,你彆著急,等我回去編好了再給你講。
於是乎,我也開始思考這個問題: 為什麼前端普遍不熟悉設計原則和設計模式呢?
說起來幾年前(做講師的時候),別說是六大設計原則,哪怕是二十三種設計模式,我也能整著背,倒著背,畢竟是靠這個傢伙吃飯。對於學生來說,這是經典的計算機理論,是最佳實踐,是乾貨,也是面試時誇誇其談的底氣。但是隨著幾年的工作,我自己也把這些定義都忘記了,我猜當年的學生們恐怕更不會背得會。背不出來的原因只有一個,那就是 這部分知識並沒有經常被使用 ,但當你把23條設計模式列出來的時候,就會發現很多的概念在前端中其實是被經常使用的。所以我們就要來找一找,問題是出在了哪裡。
比如 adapter,介面卡模式。前一段時間玩了 bearychat 的機器人開發,用到了 hubot-bearychat,就是一個介面卡。因為各個第三方服務商之間未必會提供一致的介面,需要有一箇中間層適配。
類似的,比如 parse-server 對檔案儲存可以依賴 s3 或者 qiniu,都可以通過 adapter 實現。
adapter 在物理世界裡面大概長這樣:

而 proxy 和 iterator 就是 JS 原生功能的一部分,因為相容的原因,proxy 沒有被普遍使用。順便說一下,雖然有 proxy-polyfill這個東西,但是基本等於沒有用,ES5沒法完整的模擬出 proxy。要是能用的話,vue 也不至於用 defineProperty 來實現劫持。
decorator 大家都說是 ES7 提出的特性,但是去查一下文件,發現都9102年了,還是個 stage2 draft。不過用 babel 搶跑基本沒啥問題。
觀察者模式,或者說釋出訂閱模式,有些人說是一回事,有些人說後者是前者的實現。對於前端來說,這個模式是學習 DOM 的第一課。前端不僅有事件的訂閱,還有特殊的傳播機制。而且因為有大量的非同步場景,所以除了常見的維護訂閱列表主動推送這種方式以外,前端也很常見惰性訂閱的模式,即訂閱方不會即時的對釋出內容作出響應,只有在需要使用的時候才進行拉取。而rxjs 算是把觀察者模式用到了極致,通過一個模式,解決了複雜的資料依賴問題。
惰性這個詞,在設計模式裡也有提到,singleton 單例模式就有 lazy 和 eager 兩種實現。吐個槽,不知道是把 eager 翻譯成了餓漢,所以 lazy 就變成了懶漢,不知道 lazy load 是不是要翻譯成「懶漢讀取」。
在 JS 中提單例往往也很 DT,因為在 JAVA 中只有類的例項,哪怕只有一個例項存在,也要先定義類,再例項化,而且要保證只有一個,還要執行緒安全。在 JS 中,反手一個字面量宣告,哪個物件不是獨一無二的。基於語言談單例,意義不大,但是這個理念在開發的時候還是能用上的。
比如做一個彈窗元件,需要全域性唯一,也就是說,同時最多隻能有一個彈窗。你可以搞一個先寫好 DOM,隱藏起來,需要的時候再展示的餓漢式彈窗,也可以寫一端用 js 語句描述 DOM 的程式碼,在被呼叫時再展示的懶漢式彈窗。
餓漢彈窗易於除錯,懶漢彈窗更加靈活易用。這算是前端對單例模式的另一種解讀吧。
說了這麼多,我們追根溯源來看。GoF 在1995年的著作中提出了設計模式,巧的是,布蘭登艾克也是在1995年用十天的時間創造了 javascript。所以四巨頭也無法預知到,網際網路開發會在未來大行其道,況且,設計模式本來就是針對面向物件和靜態語言提出的,把這個靜態語言的最佳實踐,套入動態語言和函數語言程式設計中,難免會有很多水土不服。
我們可以看到,眾多的設計模式已經成為了語言的一部分,也正因如此,我們才能把更多的精力投入到資料和業務中。另外說到傳統軟體業,軟體是作為一種工具,既然是工具,和錘子扳手一樣,工具都是實體的拓展和延伸,所以以面向物件來構建是恰到好處的。
另外傳統軟體業是進行一次統一交付,所以需求是封閉的,開發者往往通過構建精巧的架構去實現各種系統功能,架構的總體原則是要對協同開發友好,對版本迭代友好。這意味著別人搭的磚你最好不要動,你可以在基礎上擴充套件。既然如此,那「架構」就十分重要了,因為所有的拓展都是依附於這個框架的,框架搭的好,同時大家遵循開閉原則,可以使得專案保持清晰。
對於前端來說,開閉是天然的,里氏代換原則其實就是 Duck Type。
而且因為交付方式的不同,對於網際網路開發來說,只寫「恰到好處」的程式碼往往不是最佳實踐。介面資料要不要考慮冗餘,大量的單例元件還需要遵循單一職責原則麼?當你難以預期部分程式碼是否會被複用的時候,拆分可能意味著提前優化,而把元件拆得零散,又是違反了最小知識原則。說到最小知識,倡導只和朋友通訊,可是父子通訊的元件設計用起來非常的笨重。這麼說起來,redux 好像是個反模式的東西。但 store.dispatch 很明顯是個命令模式,state又可以說是狀態模式。
所以你看,前端不是不用設計模式,而是已經把設計模式融入到了開發的基礎當中。
至於說六條設計原則:
高 層模組不依賴底層模組,即為依賴倒轉原則。
內 部修改關閉,外部擴充套件開放,即開閉原則。
聚 合單一功能,即為單一直責原則。
低 知識要求,對外介面簡單,即迪米特法則。
耦 合多個介面,不如獨立拆分,即介面隔離原則。
合 成複用,儘量不使用繼承,即合成複用原則。
設計原則是通用的,但是要看其理念而不是形式,把傳統軟體開發時代的最佳實踐放在前端的環境中,並不是完全適用的。至於說23條設計模式,大半都已經融入到日常開發之中了,作為語言的基礎,並不是什麼值得炫耀的難點知識。
這些模式和原則可以作為知識整合的脈絡和程式碼優化的理念來看,但偏要以 JS 語法去實踐一些設計模式,並沒有什麼實際的工程意義。
「好的,你要的解釋我已經編好了」,前兩天我把這些內容發給了朋友。「況且我覺得,恐怕也不會真的有做FE的朋友能把23條從頭到尾都背一遍。」
「如果有這樣基礎紮實的人呢?」
「怕不是剛剛背好要去面試的。」