前言:框架模式不是一門寫程式碼的學問,而是一門管理與組織程式碼的學問。其本質是一種軟體開發的模型。與設計模式不同,設計模式是在解決一類問題時總結抽象出的公共方法(工廠模式,介面卡模式,單例模式,觀察者模式……),他們與某種具體的技術棧無關。一種框架模式往往使用了多種設計模式,切不要把他們的關係搞混。
更多資訊可以看看這本《Developing Backbone Application》
一代目: 指令碼式設計(無架構設計):
下面這樣的程式碼,就是無任何設計模式的產物:
1 2 3 4 5 6 7 8 9 |
const a = document.createElement("a"); a.innerHTML = "www.google.com"; a.href = "//www.google.com"; a.style.position = "absolute"; a.style.top = 100; a.onclick = function(){ console.log("google"); } document.body.appendChild(a); |
嗯嗯~先別吐槽,因為這種編碼方式還是有他的優點的。
短短的幾行程式碼,包含了建立,樣式,繫結,插入。 balabala。。。。。
這種搞法雖有不少缺點,但麻雀雖小五臟俱全,所有功能一應俱全。早些年由於UI程式還處在一個懵懂期, 邏輯不算太複雜,程式碼量也不會太多。這樣的搞法似乎也沒有什麼問題。 畢竟到達A B兩點最短的距離就是直線,上述程式碼可以說是實現某功能的最短路徑。 典型的例子就是 ASM (雖然組合語言不是用來寫UI的),他們共有的缺點是 :入口單一 功能簡單 不可維護
讓我們修改一下上面的邏輯:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
function doCss(a) { a.style.position = "absolute"; a.style.top = 100; } function doEvent(a) { a.onclick = function(){ alert("google"); } } function doAttribute(a){ a.innerHTML = "www.google.com"; a.href = "//www.google.com"; } const a = document.createElement("a"); doCss(a); doEvent(a); doAttribute(a) document.body.appendChild(a); |
這裡我們將一個功能拆分成了3個部分,即 外觀 事件 和屬性。
函式將他們重新分離成一個個獨立的邏輯塊,這樣一定程度上達到了分離複用的目的,比如你想修改外觀,就去doCss函式裡去找。。。
就像有錢人追求更多的財富,權貴追求更多權利一樣。
人類總在思考同一個問題, 我們能不能做得更好。。
直到有一天。。。
二代目: 程式碼檔案分離(CodeBehind):
對asp。net尤其是webForm,winForm模式熟悉的同學,肯定對aspx 和 aspx。cs 這2種檔案非常瞭解。
aspx是檢視檔案,而對應的。cs檔案是他的相關邏輯處理檔案 事件驅動模式下,框架幫我們完成了基本的事件型別,我們要做的是在事件下完成相關業務邏輯。
在前端中,也可以找到二代目大人的影分身。。
1 2 3 4 5 6 7 8 |
<html> <head> <link href="style.css" rel="stylesheet" /> </head> <body> <script src="bundle.js" ></script> </body> </html> |
各檔案各司其職,編寫的時候分離,在執行的時候合併。這樣進一步降低了功能之間的耦合度。 檢視看起來非常”清爽”,對應的邏輯也被分離成一個個檔案, 交由相應的開發人員處理。
如果你涉獵的技術範圍很廣,你會發現其實二代目大人已經出現在諸多成熟的技術棧中。。。。
但是,沒過多久伊甸園歡樂的笑聲被下面這個需求打破。。。
為了實現他,在這裡我只寫虛擬碼了:
1 2 3 4 5 6 7 8 9 10 11 |
`購買蘋果按鈕`繫結事件如下: 1.int 蘋果數變數 + 1; 2.顯示蘋果數控制元件的值 = 蘋果數變數; `購買梨按鈕`繫結事件如下: 1.int 梨數變數 + 1; 2.顯示梨數控制元件的值 = 梨數變數; ...`吃蘋果` ...`吃梨` |
這裡比較困難的是 第2步
就是如果顯示蘋果數的控制元件是另一個程式設計師開發的黑盒, 如何修改其值?
於是程式設計師A 去找 程式設計師B 尋求是否存在對應的 get/set 方法。
程式設計師B說有, “有” 字還沒落地, 開發買梨的程式設計師C 又踹門進來了,問了同樣的問題, 後來才知道 開發吃蘋果功能的程式設計師D正在路上。。。
於是程式設計師B 不得不把介面的詳細資訊寫到 wiki中, 於是 程式設計師CDEFGHIJKMLN 都看了wiki 懂了。
完成這件事 程式設計師B 寫wiki 花了 10分鐘, 程式設計師CDEFGHIJKMLN 看wiki每人花 1分鐘,一共團隊成本 20分鐘。
於是 wiki中這個 get/set 介面函式出現在了每一個被繫結的函式裡。一共4個button 出現4次
ps: 這個get/set介面本質上就是一個view重新整理介面
事情仍在在醞釀:
產品大爺發話了,要改成下面這樣:
多了一個求和。
於是虛擬碼變成了這樣:
1 2 3 4 5 |
`購買蘋果按鈕`繫結事件如下: 1.int 蘋果數變數 + 1; 2.int 總和變數 = 蘋果數變數 + 梨數變數; 2.顯示蘋果數控制元件的值 = 蘋果數變數; 3.顯示總和控制元件的值 = 總和變數; |
這次 程式設計師B為了人身安全,提前把 get/set 介面釋出到了 wiki上。。
於是 總和控制元件的值 = 總和變數 這行程式碼出現了 4次。
於是需求改成了這樣:
只是刪了一行。
於是 4個函式中 所有相關程式碼都被刪除。。。 共影響 4行程式碼。。
段子講完了,其核心問題在於,按照事件進行的業務模組劃分,有時候是不合理的,事件是使用者行為的入口,但不是程式邏輯的入口。 一個button的click就可能橫跨N個領域, 需要N個人來進行協作, 這部分邏輯到最後還是會耦合在一起,通過各種函式封裝進行解耦,無疑是揚湯止沸,而我們需要的是釜底抽薪
三代目: M-V-C:(模型,檢視,控制器)
現在網上有很多關於mvc的介紹,讓人糾結的是他們各不相同,而且有的根本就說的不對, 對於框架模式這東西,沒有一個嚴格的規定說這樣搞是 mvc 那樣就不是。 甚至連mvc本身也有很多變種,我們只要從根源上理解這個東西就行。
我就不扒祖墳了,我們們只需要知道它已經存在了 30多年就行了。
我們思考一下 UI(圖形化使用者介面) 的本質:
為什麼要有UI, 在計算機眼中 一切即資料,其實要是深挖這個問題,資料與操作其實都是 0 1 組成的機器碼,只不過 CPU執行的時候用指定暫存器的資料當做指令罷了,也就是說 決定一個資料到底是資料還是指令 只取決於他所在的暫存器位置。(好了好了 扯遠了,往回跑。。) 資料的操作是抽象的,是專業人士乾的事情, 計算機為了走進千家萬戶, 必須提供一種傻瓜式的操作方式,於是UI誕生了。。。 用一句話解釋UI就是:他是資料到影象的一種對映程式;
剛才說了它是一種對映程式,使用者通過操作影象上的按鈕,來達到運算元據的目的,資料被使用者改變後,肯定需要從新生成對映。
請允許我向上一張老掉牙的圖:
說說這裡面 Model View Controller 是幹什麼的:
1.View: 放置檢視相關的程式碼,原則上裡面不應該有任何業務邏輯。
2.Controller: 放置檢視與模型之間的對映,原則上這裡應該很薄,他只放一些事件繫結相關的程式碼(router),但並不實現真正的功能,他只是一個橋樑。
3.Model: 這裡的model不是說 實體類, 他是主要實現邏輯的地方。
那還是上面 買水果的例子,那麼在MVC下該如何設計呢:
概念 | 解釋 |
view層 | 放置介面程式碼,以及一些重新整理邏輯 如資料中的 0 1 轉成 男 女 |
controller層 | 放置一些繫結邏輯。完成router,不實現函式體。 |
model層 | 接收view的註冊,當自身資料變化時,執行view的重新整理函式。 業務邏輯都在這裡 |
他是這樣一個流程:
1.建立顯示蘋果數量的控制元件。
2.將上面控制元件註冊到model中。(設定關聯的資料,–蘋果數變數)
3.修改model中 蘋果數變數 。
4.由於蘋果數變數被修改,觸發所有繫結在上面的控制元件(view)從新執行重新整理函式。
5.顯示蘋果數量的控制元件被更新。
這樣便解決了大部分介面與邏輯耦合的問題,但是它並不完美:
View 和 Model 並不是完全脫離的,還是有一些邏輯耦合,因為需要根據修改後的model從新重新整理view。 難免view裡面沾染一點model的結構。
程式碼量膨脹。
不方便進行更精細的顆粒化控制。(因為view只知道 model被改了,但不知道誰改的!)
- model在對應多個view的時候,很難都伺候到位。
於是。。。
四代目: M-V-P:(模型,檢視,派發器)
請允許我再上一張老圖:
針對mvc的一些問題,在mvp模式下, 斬斷了 view 與 model的關係, 當m 改變時,m 通知 p 去改變v, 所以v變得更純潔(重新整理邏輯被移動到了p層), 為了保證m可以最大程度的複用 一部分業務邏輯也從 m 轉移到了p所以 mvp下 p 非常厚實。
mvp中最後改變v的是p那麼在 v與p 之間會有一個介面,解決怎麼轉換以及傳值的問題。
五代目: M-V-VM:(模型,檢視,抽象檢視)
MVVM,最早來自於微軟社群,用於WPF
Model-View-ViewModel的關係圖:
mvvm 與 mvp 的最大區別就是它使用 資料繫結(Data Binding)、依賴屬性(Dependency Property)、命令(Command)、路由事件(Routed Event) 來搞定與view層的互動, 但是這種繫結是與某種具體技術棧相關的, ViewModel從Model中抽象而來,但更貼近於業務模型, 比如你Model中某欄位是 true false, ViewModel中可能就是 “黑”,”白”等 這種更貼近業務場景的描述。 ViewModel中的屬性直接與某具體控制元件的屬性相繫結。 也就是說當某具體控制元件發生變化,ViewModel中的 某個欄位就會跟著變化,然後Model中的欄位也會進一步變化。
以上述為例:
使用者使用UI修改了性別欄位:
1.操作觸發繫結在UI上的事件(Data Binding 自動完成)
2.事件進入vm層,根據繫結規則,找出對應的vm欄位, (如表示性別的元件繫結的是vm中的sex欄位)
3.vm上的sex被設定成true,(view層上值為”男” “女”,但是在抽象的vm層中我們用 bool 來表示這個欄位)
4.同理尋找m層上的對應欄位,m上的sex被vm修改成1
5.m找到所有與sex欄位有繫結關係的vm通知他們更新。
6.所有接到通知的vm更新sex欄位。
7.vm尋找所有與sex欄位有繫結關係的view層控制元件,通知他們更新(Data Binding 自動完成)。
8.view被更新。所有涉及到sex欄位的元件都被重新整理。
有時候這個流程未必是從 1 步開始,如果直接對 m 進行修改,則就是從第 4 步開始的。
同理如果沒有view層,則沒有必要進行 7 , 8步驟。
這就是說 mvvm 下可以完全乾掉 view 層, 方便的進行自動化測試。
小結 (推送/訂閱 這個是資料驅動的核心)
不管是 mvc 還是 mvp 或 mvvm ,他們都是 資料驅動 的。核心上基於 m 推送訊息,v或p來訂閱 這個模型。使用者需要維護的不再是 UI 樹,而是抽象的資料。(通過資料,可以隨時構建出新的 UI 樹), 當 UI 的狀態一旦多起來,這種框架模式的優勢便體現出來了。 因為維護資料可比維護 UI 狀態爽多了。
前端中的mvc:
並不是說 m v c 三者一定要獨立出現才行,比如 Backbone。js 它的 controller 層只是一個 router。 其實在傳統 mvc 中 controller 裡本來就沒有太多的邏輯,他只是 一個事件的”傳遞者”, 加之 javascript 中人們習慣使用匿名函式當事件回撥,這樣就等於直接在 view 層中把功能函式實現了。 所以 view 與 controller 合併 或者 controller 與model 合併都有可能。
1 2 3 |
$(xxx).click(function(e){ console.log(e); }) |
打賞支援我寫出更多好文章,謝謝!
打賞作者
打賞支援我寫出更多好文章,謝謝!