轉(zhuǎn)自:掘金 iOS一葉
iOS APP 架構(gòu)設計
一,APP架構(gòu)概述
應用架構(gòu)
2.Model 和 View
3 ^[1]^ . App 的本質(zhì)是反饋回路
4.架構(gòu)技術(shù)
5.App^[2]^ 任務
6.iOS 架構(gòu)的5中模式:
二,APP設計常用的5種模式概覽
Model-View-Controller
Model-View-ViewModel+協(xié)調(diào)器
Model-View-Controller+ViewState
Model 適配器-View 綁定器 (MAVB)
Elm 架構(gòu) (TEA)
三,其他APP架構(gòu)模式
Model-View-Presenter
VIPER,Riblets,和其他 “Clean” 架構(gòu)
一,APP架構(gòu)概述
1. 應用架構(gòu)
App 架構(gòu)是軟件設計的一個分支,它關(guān)心如何設計一個 app 的結(jié)構(gòu)。具體來說,它關(guān)注于兩個 方面:如何將 app 分解為不同的接口和概念層次部件,以及這些部件之間和自身的不同操作中 所使用的控制流和數(shù)據(jù)流路徑。
我們通常使用簡單的框圖來解釋 app 的架構(gòu)。比如,Apple 的 MVC 模式可以通過 model、 view 和 controller 三層結(jié)構(gòu)來描述。
上面框圖中的模塊展示了這個模式中不同名字的三個層次。在一個 MVC 項目中,絕大部分的代 碼都會落到其中某個層上。箭頭表示了這些層進行連接的方式。
但是,這種簡單的框圖幾乎無法解釋在實踐中模式的操作方式。這是因為在實際的 app 架構(gòu)中, 部件的構(gòu)建有非常多的可能性。事件流在層中穿梭的方式是什么?部件之間是否應該在編譯期 間或者運行時持有對方?要怎么讀取和修改不同部件中的數(shù)據(jù)?以及狀態(tài)的變更應該以哪條路 徑在 app 中穿行?
2.Model 和 View
在最高的層級上,app 架構(gòu)其實就是一套分類,app 中不同的部件會被歸納到某個類型中去。在本書中,我們將這些不同的種類叫做層次:一個層次指的是,遵循一些基本規(guī)則并負責特定 功能的接口和其他代碼的集合。
Model 層和 View 層是這些分類中最為常?的兩個。
Model 層是 app 的內(nèi)容,它不依賴于 (像 UIKit 那樣的) 任何 app 框架。也就是說,程序員對 model 層有完全的控制。Model 層通常包括 model 對象 (在錄音 app 中的例子是文件夾和錄音對象) 和協(xié)調(diào)對象 (比如我們的 app 例子中的負責在磁盤上存儲數(shù)據(jù)的 Store 類型)。被存儲在 磁盤上的那部分 model 我們稱之為文檔 model (documentation model)。
View 層是依賴于 app 框架的部分,它使 model 層可?,并允許用戶進行交互,從而將 model 層轉(zhuǎn)變?yōu)橐粋€ app。當創(chuàng)建 iOS 應用時,view 層幾乎總是直接使用 UIKit。不過,我們也會看 到在有些架構(gòu)中,會使用 UIKit 的封裝來實現(xiàn)不同的 view 層。另外,對一些其他的像是游戲這 樣的自定義應用,view 層可以不是 UIKit 或者 AppKit,它可能是 SceneKit 或者 OpenGL 的某 種封裝。
有時候,我們選擇使用結(jié)構(gòu)體或者枚舉來表示 model 或者 view 的實例,而不使用類的對象。在實踐中,類型之間的區(qū)別非常重要,但是當我們在 model 層中談到對象、結(jié)構(gòu)體和枚舉時, 我們會將三者統(tǒng)一地稱為 model 對象。類似地,我們也會把 view 層的實例叫做 view 對象,實 際上它們也可能是對象、結(jié)構(gòu)體或者枚舉。
View 對象通常會構(gòu)成一個單一的 view 層級,在這個層級中,所有的 view 對象通過樹結(jié)構(gòu)的方 式連接起來。在樹的根部是整個屏幕,屏幕中存在若干窗口,接下來在樹的分支和葉子上是更 多的小 view。類似地,view controller 也通常會形成 view controller 層級。不過,model 對 象卻不需要有明確的層級關(guān)系,在程序中它們可以是互不關(guān)聯(lián)的獨立 model。
當我們提到 view 時,通常指的是像一個按鈕或者一個文本 label 這樣的單一 view 對象。當我 們提到 model 時,我們通常指的也是像一個 Recording 實例或者 Folder 實例這樣的單個 model 對象。在該話題的大多數(shù)文獻中,“model” 在不同上下文中指的可能是不同的事情。它 可以指代一個 model 層,model 層中的具體的若干對象,文檔 model,或者是 model 層中不 關(guān)聯(lián)的文檔。雖然可能會顯得啰嗦,我們還是會嘗試在本書中盡量明確地區(qū)分這些不同含義。
為什么 Model 和 View 的分類會被認為是基礎中的基礎
當然啦,就算不區(qū)分 model 層和 view 層,寫出一個 app 也是絕對可能的。比如說,在一個簡 單的對話框中,通常就沒有獨立的 model 數(shù)據(jù)。在用戶點擊 OK 按鈕的時候,我們可以直接從 用戶界面元素中讀取狀態(tài)。不過通常情況下,model 層的缺失,會讓程序的行為缺乏對于清晰 規(guī)則的依據(jù),這會使得代碼難以維護。
定義一個 model 層的最重要的理由是,它為我們的程序提供一個表述事實的單一來源,這會讓 邏輯清晰,行為正確。這樣一來,我們的程序便不會被應用框架中的實現(xiàn)細節(jié)所支配。
應用框架為我們提供了構(gòu)建 app 所需要的基礎設施。在本書中,我們使用 Cocoa - 或者更精確 說,根據(jù)目標平臺,使用 UIKit,AppKit 或者 WatchKit - 來作為應用框架。
如果 model 層能做到和應用框架分離,我們就可以完全在 app 的范圍之外使用它。我們可以很 容易地在另外的測試套件中運行它,或者用一個完全不同的應用框架重寫新的 view 層。這個 model 層將能夠用于 Android,macOS 或者 Windows 版本的 app 中。
3.App 的本質(zhì)是反饋回路
View 層和 model 層需要交流。所以,兩者之間需要存在連接。假設 view 層和 model 層是被清
晰地分開,而且不存在無法解耦的聯(lián)結(jié)的話,兩者之間的通訊就需要一些形式的翻譯:
從根本上說,用戶界面是一個同時負責展示和輸入功能的反饋設備,所以毫無疑問,這導致的 結(jié)果就是一個反饋回路。每個 app 設計模式所面臨的挑戰(zhàn)是如何處理這張圖表中箭頭所包含的 交流,依賴和變換。
在 model 層和 view 層之間不同的路徑擁有不同的名字。用戶發(fā)起的事件會導致 view 的響應, 我們把由此引起的代碼路徑稱為 view action,像是點擊按鈕或者選中 table view 中的某一行 就屬于 view action。當一個 view action 被送到 model 層時,它會被轉(zhuǎn)變?yōu)?model action (或 者說,讓 model 對象執(zhí)行一個 action 或者進行更新的命令)。
這種命令也被叫做一個消息 (特別 在當 model 是被 reducer 改變時,我們會這么稱呼它)。將 view action 轉(zhuǎn)變?yōu)?model action 的操作,以及路徑上的其他邏輯被叫做交互邏輯。
一個或者多個 model 對象上狀態(tài)的改變叫做 model 變更。Model 的變更通常會觸發(fā)一個model 通知,比如說從 model 層發(fā)出一個可觀測的通知,它描述 model 層中什么內(nèi)容發(fā)生了 改變。當 view 依賴于 model 數(shù)據(jù)時,通知會觸發(fā)一個 view 變更,來更改 view 層中的內(nèi)容。
這些通知可以以多種形式存在:Foundation 中的 Noti?cation,代理,回調(diào),或者是其他機制, 都是可以的。將 model 通知和數(shù)據(jù)轉(zhuǎn)變?yōu)?view 更改的操作,以及路徑上的其他邏輯被叫做表 現(xiàn)邏輯。
根據(jù) app 模式的不同,有些狀態(tài)可能是在文檔 model 之外進行維護的,這樣一來,更新這些狀 態(tài)的行為就不會追隨文檔 model 的路徑。在很多模式中的導航狀態(tài)就這種行為的一個常?例 子,在 view 層級中的某個部分 (或者按照 Cocoa Storyboard 中使用的術(shù)語,將它稱為 scene) 可能會被換出或者換入層級中。
在 app 中非文檔 model 的狀態(tài)被叫做 view state。在 Cocoa 里,大部分 view 對象都管理著它 們自己的 view state,controller 對象則管理剩余的 view state。在 Cocoa view state 的框圖 中,通常會有加在反饋回路上的捷徑,或者單個層自身進行循環(huán)。在有一些架構(gòu)中,view state 不屬于 controller 層,而是屬于 model 層的部分 (不過,根據(jù)定義,view controller 并不是文 檔 model 的一部分)。
當所有的狀態(tài)都在 model 層中被維護,而且所有的變更都通過完整的反饋回路路徑進行傳遞 時,我們就將它稱為單向數(shù)據(jù)流。當任意的 view 對象或者中間層對象只能夠通過 model 發(fā)出 的通知來進行創(chuàng)建和更新 (換句話說,view 或者中間層不能通過捷徑來更新自身或者其他的 view) 時,這個模式通常就是單向的。
4.架構(gòu)技術(shù)
Apple 平臺的標準 Cocoa 框架提供了一些架構(gòu)工具。Noti?cation 將值從單一源廣播給若干個 收聽者。鍵值觀察 (KVO) 可以將某個對象上屬性的改變報告給另一個對象。然而,Cocoa 中的 架構(gòu)工具十分有限,我們將會使用到一些額外的框架。
本書中使用到的第三方技術(shù)中包含了響應式編程。響應式編程也是一種用來交流變更的工具, 不過和通知或者 KVO 不同的是,它專注于在源和目標之間進行變形,讓邏輯可以在部件之間傳 輸信息的同時得以表達。
我們可以使用像是響應式編程或者 KVO 這樣的技術(shù)創(chuàng)建屬性綁定。綁定接受一個源和一個目 標,無論何時,只要源發(fā)生了變化,目標也將被更新。這和手動進行觀察在語法上有著不同, 我們不再需要寫觀察的邏輯,而只需要指定源和目標,接下來框架將會為我們處理其余部分的 工作。
macOS 上的 Cocoa 包含有 Cocoa 綁定技術(shù),它是一種雙向綁定,所有的可觀察對象同時也是 觀察者,在一個方向上建立綁定連接,會在反方向也創(chuàng)建一個連接。不論是 (在MVVM-C 的章
節(jié)中用到的) RxCocoa,還是 (MAVB 章節(jié) 中用到的) CwlViews,都不是雙向綁定的。所以,在
本書中,所有關(guān)于綁定的討論都只涉及到單向綁定。
5.App任務
要讓程序正常工作,view 必須依賴于 model 數(shù)據(jù)來生成和存在,我們配置 view,讓它可以對
model 進行更改,并且能在 model 更新時也得到更新。所以,我們需要決定在 app 中如何執(zhí)行下列任務:
1.構(gòu)建—誰負責構(gòu)建model和view,以及將兩者連接起來?
2.更新model—如何處理viewaction?
3.改變view—如何將model的數(shù)據(jù)應用到view上去?
4.viewstate—如何處理導航和其他一些modelstate以外的狀態(tài)?
5.測試—為了達到一定程度的測試覆蓋,要采取怎樣的測試策略?
6.對于上面五個問題的回答,是構(gòu)成 app 設計模式的基礎要件。在本書中,我們會逐一研究這些 設計模式。
6.IOS 架構(gòu)的5中模式:
IOS 架構(gòu)的5中模式:
標準的CocoaModel-View-Controller(MVC)是Apple在示例項目中所采用的設計模 式。它是 Cocoa app 中最為常?的架構(gòu),同時也是在 Cocoa 中討論架構(gòu)時所采用的基 準線。
Model-View-ViewModel+協(xié)調(diào)器(MVVM-C)是MVC的變種,它擁有單獨的 “view-model” (視圖模型) 和一個用來管理 view controller 的協(xié)調(diào)器。MVVM 使用數(shù)據(jù) 綁定 (通常會和響應式編程一起使用) 來建立 view-model 層和 view 層之間的連接。
Model-View-Controller+ViewState(MVC+VS)這種模式將所有的viewstate集中到 一個地方,而不是讓它們散落在 view 和 view controller 中。這和 model 層所遵循的規(guī) 則相同。
Model適配器-View綁定器(ModelAdapter-ViewBinder,MAVB)是本書的一位作者所 使用的實驗性質(zhì)的架構(gòu)。MAVB 專注于構(gòu)建聲明式的 view,并且拋棄 controller,采用 綁定的方式來在 model 和 view 之間進行通訊。
Elm架構(gòu)(TEA)與MVC或者MVVM這樣的常?架構(gòu)完全背道而馳。它使用虛擬view 層級來構(gòu)建 view,并使用 reducer 來在 model 和 view 之間進行交互。
二,APP設計常用的5種模式概覽
1. Model-View-Controller
在 Cocoa MVC 中,一小部分 controller 對象負責處理 model 或者 view 層范疇之外的所有任 務。
這意味著,controller 層接收所有的 view action,處理所有的交互邏輯,發(fā)送所有的 model action,接收所有的 model 通知,對所有用來展示的數(shù)據(jù)進行準備,最后再將它們應用到 view 的變更上去。如果我們?nèi)タ匆幌陆榻B一章中的 app 反饋回路的圖,會發(fā)現(xiàn)在 model 和 view 之
間的箭頭上,幾乎每個標簽都是 controller。而且要知道,在這幅圖中,構(gòu)建和導航任務并沒有 標注出來,它們也會由 controller 來處理。
下面是 MVC 模式的框圖,它展示了一個 MVC app 的主要通訊路徑:
圖中的虛線部分代表運行時的引用,view 層和 model 層都不會直接在代碼中引用 controller。實線部分代表編譯期間的引用,controller 實例知道自己所連接的 view 和 model 對象的接口。
如果我們在這個圖標外部描上邊界的話,就得到了一個 MVC 版本的 app 反饋回路。注意在圖 表中其他的路徑并不參與整個反饋回路的路徑 (也就是 view 層和 controller 層上那些指向自身 的箭頭)。
1.構(gòu)建
App 對象負責創(chuàng)建最頂層的 view controller,這個 view controller 將加載 view,并且知道應 該從 model 中獲取哪些數(shù)據(jù),然后把它們顯示出來。Controller 要么顯式地創(chuàng)建和持有 model 層,要么通過一個延遲創(chuàng)建的 model 單例來獲取 model。在多文檔配置中,model 層由更低層 的像是 UIDocument 或 NSDocument 所擁有。那些和 view 相關(guān)的單個 model 對象,通常會 被 controller 所引用并緩存下來。
2.更改 Model
在 MVC 中,controller 主要通過 target/action 機制和 (由 storyboard 或者代碼進行設置的) delegate 來接收 view 事件。Controller 知道自己所連接的 view,但是 view 在編譯期間卻沒有 關(guān)于 controller 接口的信息。當一個 view 事件到達時,controller 有能力改變自身的內(nèi)部狀態(tài), 更改 model,或者直接改變 view 層級。
3.更改 View
在我們所理解的 MVC 中,當一個更改 model 的 view action 發(fā)生時,controller 不應該直接去 操作 view 層級。正確的做法是,controller 去訂閱 model 通知,并且在當通知到達時再更改 view 層級。這樣一來,數(shù)據(jù)流就可以單向進行:view action 被轉(zhuǎn)變?yōu)?model 變更,然后 model 發(fā)送通知,這個通知最后被轉(zhuǎn)為 view 變更。
4.View State
View state 可以按需要被 store 在 view 或者 controller 的屬性中。相對于影響 model 的 view action,那些只影響 view 或 controller 狀態(tài)的 action 則不需要通過 model 進行傳遞。對于 view state 的存儲,可以結(jié)合使用 storyboard 和 UIStateRestoring 來進行實現(xiàn),storyboard 負責記錄活躍的 controller 層級,而 UIStateRestoring 負責從 controller 和 view 中讀取數(shù)據(jù)。
5.測試
在 MVC 中,view controller 與 app 的其他部件緊密相連。邊界的缺失使得為 controller 編寫 單元測試和接口測試十分困難,集成測試是余下的為數(shù)不多的可行測試手段之一。在集成測試 中,我們構(gòu)建相連接的 view、model 和 controller 層,然后操作 model 或者 view,來測試是 否能得到我們想要的結(jié)果。
集成測試的書寫非常復雜,而且它涵蓋的范圍太廣了。它不僅僅測試邏輯,也測試部件是如何 連接的 (雖然在一些情況下和 UI 測試的?度并不相同)。不過,在 MVC 中通過集成測試,通常 達到 80% 左右的測試覆蓋率是有可能的。
MVC 的重要性
因為 Apple 在所有的實例項目中都使用了這種模式,加上 Cocoa 本身就是針對這種模式設計 的,所以 Cocoa MVC 成為了 iOS,macOS,tvOS 和 watchOS 上官方認證的 app 架構(gòu)模式。
歷史
MVC 這個名字第一次被提出是在 1979 年,Trygve Reenskaug 用它來描述 Smalltalk-76 上已 經(jīng)存在的 “template pattern” 應用。在他和 Adele Goldberg 討論了術(shù)語方面的問題后,MVC 的名字被確定下來 (之前的名字包括 Model-View-Editor 和 Model-View-Tool-Editor 等)。
在原本的構(gòu)想中,view 是直接 “附著” 在 model 層上,并觀察所有 model 變化的。Controller 存在的目的僅僅是捕捉用戶事件,并把它們轉(zhuǎn)發(fā)給 model。這兩個特性都是 Smalltalk 運行方 式的產(chǎn)物,它們并不是為了現(xiàn)代的 app 框架所設計的,所以今天這種 MVC 的原始構(gòu)想已經(jīng)幾 乎絕跡了。
Cocoa 中的 MVC 實現(xiàn)可以追溯到大約 1997 年的 NeXTSTEP 4 的年代。在那之前,所有現(xiàn)在 controller 所擔當?shù)?色,通常都由一個 (像是 NSWindow 那樣的) 高層 view 類來扮演。之后, 從原始的 Smalltalk 的 MVC 實現(xiàn)中所發(fā)展出的理念是分離展示部分,也就是 view 層和 model 層應該被完全隔離開,這帶來了一個強烈的需求,那就是要引入一個支持對象來輔助兩者之間 的通訊。
NeXTSTEP 中 controller 的概念和 Taligent 稍早的 Model-View-Presenter 中的 presenter (展示器) 很相似。不過,在現(xiàn)在 Model-View-Presenter 這個名字通常被用來指代那 些通過協(xié)議從 controller 中將 view 抽象出來的類似 MVC 的模式。
2. Model-View-ViewModel+協(xié)調(diào)器
MVVM 和 MVC 類似,也是通過基于場景 (scene,view 層級中可能會在導航發(fā)生改變時切入或者換出的子樹) 進行的架構(gòu)。相較于 MVC,MVVM 在每個場景中使用 view-model 來描述場景中的表現(xiàn)邏輯和交互邏輯。
View-model 在編譯期間不包含對 view 或者 controller 的引用。它暴露出一系列屬性,用來描 述每個 view 在顯示時應有的值。把一系列變換運用到底層的 model 對象后,就能得到這些最 終可以直接設置到 view 上的值。實際將這些值設置到 view 上的工作,則由預先建立的綁定來 完成,綁定會保證當這些顯示值發(fā)生變化時,把它設定到對應的 view 上去。響應式編程是用來 表達這類聲明和變換關(guān)系的很好的工具,所以它天生就適合 (雖說不是嚴格必要) 被用來處理
view-model。在很多時候,整個 view-model 都可以用響應式編程綁定的方式,以聲明式的形 式進行表達。
在理論上,因為 view-model 不包含對 view 層的引用,所以它是獨立于 app 框架的,這讓對于 view-model 的測試也可以獨立于 app 框架。
由于 view-model 是和場景耦合的,我們還需要一個能夠在場景間切換時提供邏輯的對象。在 MVVM-C 中,這個對象叫做協(xié)調(diào)器 (coordinator)。協(xié)調(diào)器持有對 model 層的引用,并且了解 view controller 樹的結(jié)構(gòu),這樣,它能夠為每個場景的 view-model 提供所需要的 model 對象。
和 MVC 不同,MVVM-C 中的 view controller 從來都不會直接引用其他的 view controller (所 以,也不會引用其他的 view-model)。View controller 通過 delegate 的機制,將 view action 的信息告訴協(xié)調(diào)器。協(xié)調(diào)器據(jù)此顯示新的 view controller 并設置它們的 model 數(shù)據(jù)。換句話 說,view controller 的層級是由協(xié)調(diào)器進行管理的,而不是由 view controller 來決定的。
如果我們忽略掉協(xié)調(diào)器,那么這張圖表就很像 MVC 了,只不過在 view controller 和 model 之 間加入了一個階段。MVVM 將之前在 view controller 中的大部分工作轉(zhuǎn)移到了 view-model 中,但是要注意,view-model 并不會在編譯時擁有對 view controller 的引用。
View-model 可以從 view controller 和 view 中獨立出來,也可以被單獨測試。同樣,view controller 也不再擁有內(nèi)部的 view state,這些狀態(tài)也被移動到了 view-model 中。在 MVC 中 view controller 的雙重?色 (既作為 view 層級的一部分,又負責協(xié)調(diào) view 和 model 之間的交 互),減少到了單一?色 (view controller 僅僅只是 view 層級的一部分)。
協(xié)調(diào)器模式的加入進一步減少了 view controller 所負責的部分:現(xiàn)在它不需要關(guān)心如何展示其 他的 view controller 了。因此,這實際上是以添加了一層 controller 接口為代價,降低了 view controller 之間的耦合。
1.構(gòu)建
對于 model 的創(chuàng)建和 MVC 中的保持不變,通常它是一個頂層 controller 的職責。不過,單獨
的 model 對象現(xiàn)在屬于 view-model,而不屬于 view controller。
初始的 view 層級的創(chuàng)建和 MVC 中的一樣,通過 storyboard 或者代碼來完成。和 MVC 不同的 是,view controller 不再直接為每個 view 獲取和準備數(shù)據(jù),它會把這項工作交給 view-model。View controller 在創(chuàng)建的時候會一并創(chuàng)建 view-model,并且將每個 view 綁定到 view-model 所暴露出的相應屬性上去。
2.更改 Model
在 MVVM 中,view controller 接收 view 事件的方式和 MVC 中一樣 (在 view 和 view controller 之間建立連接的方式也相同)。不過,當一個 view 事件到達時,view controller 不會 去改變自身的內(nèi)部狀態(tài)、view state、或者是 model。相對地,它立即調(diào)用 view-model 上的方 法,再由 view-model 改變內(nèi)部狀態(tài)或者 model。
3.更改 View
和 MVC 不同,view controller 不監(jiān)聽 model。View-model 將負責觀察 model,并將 model 的通知轉(zhuǎn)變?yōu)?view controller 可以理解的形式。View controller 訂閱 view-model 的變更,這 通常通過一個響應式編程框架來完成,但也可以使用任意其他的觀察機制。當一個 view-model 事件來到時,由 view controller 去更改 view 層級。
為了實現(xiàn)單向數(shù)據(jù)流,view-model 總是應該將變更 model 的 view action 發(fā)送給 model,并 且僅僅在 model 變化實際發(fā)生之后再通知相關(guān)的觀察者。
4.View State
View state 要么存在于 view 自身之中,要么存在于 view-model 里。和 MVC 不同,view controller 中不存在任何 view state。View-model 中的 view state 的變更,會被 controller 觀 察到,不過 controller 無法區(qū)分 model 的通知和 view state 變更的通知。當使用協(xié)調(diào)器時, view controller 層級將由協(xié)調(diào)器進行管理。
5.測試
因為 view-model 和 view 層與 controller 層是解耦合的,所以可以使用接口測試來測試 view-model,而不需要像 MVC 里那樣使用集成測試。接口測試要比集成測試簡單得多,因為 不需要為它們建立完整的組件層次結(jié)構(gòu)。
為了讓接口測試盡可能覆蓋更多的范圍,view controller 應當盡可能簡單,但是那些沒有被移 出 view controller 的部分仍然需要單獨進行測試。在我們的實現(xiàn)中,這部分內(nèi)容包括與協(xié)調(diào)器 的交互,以及初始時負責創(chuàng)建工作的代碼。
MVVM 的重要性
MVVM 是 iOS 上最流行的 MVC 的非直接變形的 app 設計模式。換言之,它和 MVC 相比,并沒有非常大的不同;兩者都是圍繞 view controller 場景構(gòu)建的,而且所使用的機制也大都相同。
最大的區(qū)別可能在于 view-model 中對響應式編程的使用了,它被用來描述一系列的轉(zhuǎn)換和依 賴關(guān)系。通過使用響應式編程來清晰地描述 model 對象與顯示值之間的關(guān)系,為我們從總體上 理解應用中的依賴關(guān)系提供了重要的指導。
iOS 中的協(xié)調(diào)器是一種很有用的模式,因為管理 view controller 層級是一件非常重要的事情。協(xié)調(diào)器在本質(zhì)上并沒有和 MVVM 綁定,它也能被使用在 MVC 或者其他模式上。
歷史
MVVM 由 Ken Cooper 和 Ted Peters 提出,他們當時在微軟工作,負責后來變成 Windows Presentation Foundation (WPF) 的項目,這是微軟.NET^[3]^ 的 app 框架,并于 2005 年正式發(fā)布。
WPF 使用一種基于 XML,被稱為 XAML 的描述性語言來描述 view 所綁定的某個 view-model 上的屬性。在 Cocoa 中,沒有 XAML,我們必須使用像是 RxSwift 這樣的框架和一些 (通常存 在于 controller 中的) 代碼來完成 view-model 和 view 的綁定。
MVVM 和我們在 MVC 歷史中提到的 MVP 模式非常類似. 不過,在 Cooper 和 Peters 的論述中, MVVM 中 view 和 view-model 的綁定需要明確的框架支持,但 presenter 是通過傳統(tǒng)的手動 方式來傳遞變化。
iOS 中的協(xié)調(diào)器則是最近才 (重新) 流行起來的,Soroush Khanlou 在 2015 年時在他的網(wǎng)站上描述了這個想法。協(xié)調(diào)器基于 app controller 這樣的更古老的模式,而它們在 Cocoa 和其他平臺上已經(jīng)存在了有數(shù)十年之久。
3. Model-View-Controller+ViewState
MVC+VS 是為標準的 MVC 帶來單向數(shù)據(jù)流方式的一種嘗試。在標準的 Cocoa MVC 中,view state 可以由兩到三種不同的路徑進行操作,MVC+VS 則試圖避免這點,讓 view state 的處理 更加易于管理。在 MVC+VS 中,我們明確地在一個新的 model 對象中,對所有的 view state 進行定義和表達,我們把這個對象叫做 view state model。
在 MVC+VS 中,我們不會忽略任何一次導航變更,列表選擇,文本框編輯,開關(guān)變更,model 展示或者滾動位置變更 (或者其他任意的 view state 變化)。我們將這些變更發(fā)送給 view state model。每個 view controller 負責監(jiān)聽 view state model,這樣變更的通訊會非常直接。在表現(xiàn)或者交互邏輯部分,我們不從 view 中去讀取 view state ,而是從 view state model 中去獲 取它們:
結(jié)果所得到的圖表和 MVC 類似,但 controller 的內(nèi)部反饋回路的部分 (被用來更新 view state) 有所不同,現(xiàn)在它和 model 的回路類似,形成了一個獨立的 view state 回路。
1.構(gòu)建
和傳統(tǒng)的 MVC 一樣,將文檔 model 數(shù)據(jù)應用到 view 上的工作依然是 view controller 的責任, view controller 還會使用和訂閱 view state 。因為 view state model 和文檔 model 都需要觀 察,所以相比于典型的 MVC 來說,我們需要多得多的通過通知進行觀察的函數(shù)。
2.更改 Model
當 view action 發(fā)生時,view controller 去變更文檔 model (這和 MVC 保持不變) 或者變更 model state。我們不會去直接改變 view 層級,所有的 view 變更都要通過文檔 model 和 view state model 的通知來進行。
3.更改 View
Controller 同時對文檔 model 和 view state model 進行觀察,并且只在變更發(fā)生的時候更新 view 層級。
View State
View State 被明確地從 view controller 中提取出來。處理的方法和 model 是一樣的: controller 觀察 view state model,并且對應地更改 view 層級。
4.測試
在 MVC+VS 中,我們使用和 MVC 里類似的集成測試,但是測試本身會非常不同。所有的測試 都從一個空的根 view controller 開始,然后通過設定文檔 model 和 view state model,這個 根 view controller 可以構(gòu)建出整個 view 層級和 view controller 層級。MVC 的集成測試中最困 難的部分 (設定所有的部件) 在 MVC+VS 中可以被自動完成。要測試另一個 view state 時,我 們可以重新設置全局 view state,所有的 view controller 都會調(diào)整自身。
一旦 view 層級被構(gòu)建,我們可以編寫兩種測試。第一種測試負責檢查 view 層級是不是按照我 們的期望被建立起來,第二種測試檢查 view action 有沒有正確地改變 view state。
MVC+VS 的重要性
MVC+VS 主要是用來對 view state 進行教學的工具。
在一個非標準 MVC 的 app 中,添加一個 view state model,并且在每個 view controller 中 (在已經(jīng)對 model 進行觀察的基礎上) 觀察這些 view state model,提供了不少優(yōu)點:任意的狀 態(tài)恢復 (這種恢復不依賴于 storyboard 或者 UIStateRestoration),完整的用戶界面日志,以及 為了調(diào)試目的,在不同的 view state 間進行跳轉(zhuǎn)的能力。
歷史
這種特定的體系是 Matt Gallagher 在 2017 年開發(fā)的教學工具,它被用來展示單向數(shù)據(jù)流和用 戶界面的時間旅行等概念。這個模式的目標是,在傳統(tǒng)的 Cocoa MVC app 上通過最小的改動, 實現(xiàn)對 view 的狀態(tài)在每個 action 發(fā)生時都可以進行快照。
4. Model 適配器-View 綁定器 (MAVB)
MAVB 是一種以綁定為中心的實驗模式。在這個模式中,有三個重要的概念:view 綁定器, model 適配器,以及綁定。
View 綁定器是 view (或者 view controller) 的封裝類:它構(gòu)建 view,并且為它暴露出一個綁定 列表。一些綁定為 view 提供數(shù)據(jù) (比如,一個標簽的文本),另一些從 view 中發(fā)出事件 (比如, 按鈕點擊或者導航變更)。
雖然 view 綁定器可以含有動態(tài)綁定,但是 view 綁定器本身是不可變的。這讓 MAVB 也成為了 一種聲明式的模式:你聲明 view 綁定器和它們的 action,而不是隨著時間去改變 view 綁定器。
Model 適配器是可變狀態(tài)的封裝,它是由所謂的 reducer 進行實現(xiàn)的。Model 適配器提供了一 個 (用于發(fā)送事件的) 輸入綁定,以及一個 (用于接收更新的) 輸出綁定。
在 MAVB 中,你不會去直接創(chuàng)建 view;相對地,你只會去創(chuàng)建 view 綁定器。同樣地,你也從 來不會去處理 model 適配器以外的可變狀態(tài)。在 view 綁定器和 model 適配器之間的 (兩個方 向上的) 變換,是通過 (使用標準的響應式編程技術(shù)) 來對綁定進行變形而完成的。
MAVB 移除了對 controller 層的需求。創(chuàng)建邏輯通過 view 綁定器來表達,變換邏輯通過綁定來 表達,而狀態(tài)變更則通過 model 適配器來表達。結(jié)果得到的框圖如下:
1.構(gòu)建
Model 適配器 (用來封裝主 model ) 和 view state 適配器 (封裝頂層的 view state) 通常是在
main.swift 文件中進行創(chuàng)建的,這早于任何的 view。
View 綁定器使用普通的函數(shù)進行構(gòu)建,這些函數(shù)接受必要的 model 適配器作為參數(shù)。實際的
Cocoa view 則由框架負責進行創(chuàng)建。2. 更改 Model
當一個 view (或者 view controller) 可以發(fā)出 action 時,對應的 view 綁定允許我們指定一個 action 綁定。在這里,數(shù)據(jù)從 view 流向 action 綁定的輸出端。典型情況下,輸出端會與一個 model 適配器相連接,view 事件會通過綁定進行變形,成為 model 適配器可以理解的一條消 息。這條消息隨后被 model 適配器的 reducer 使用,并改變狀態(tài)。
2.更改 View
當 model 適配器的狀態(tài)發(fā)生改變時,它會通過輸出信號產(chǎn)生通知。在 view 綁定器中,我們可 以將 model 適配器的輸出信號進行變形,并將它綁定到一個 view 屬性上去。這樣一來,view 屬性就會在一個通知被發(fā)送時自動進行變更了。
3.View State
View state 被認為是 model 層的一部分。View state action 以及 view state 通知和 model action 以及 model 通知享有同樣的路徑。
4.測試
在 MAVB 中,我們通過測試 view 綁定器來測試代碼。由于 view 綁定器是一組綁定的列表,我 們可以驗證綁定包含了我們所期望的條目,而且它們的配置正確無誤。我們可以和使用綁定來 測試初始構(gòu)建以及發(fā)生變化時的情況。
在 MAVB 中進行的測試,與在 MVVM 中的測試很相似。不過,在 MVVM 中,view controller 有可能會包含邏輯,這導致在 view-model 和 view 之間有可能會存在沒有測試到的代碼。而 MAVB 中不存在 view controller,綁定代碼是 model 適配器和 view 綁定器之間的唯一的代碼, 這樣一來,保證完整的測試覆蓋要簡單得多。
MAVB 的重要性
在我們所討論的主要模式之中,MAVB 沒有遵循某個直接的先例,它既不是從其他平臺移植過 來的模式,也不是其他模式的變種。它自成一派,用于試驗目的,而且一些奇怪。我們在這兒 介紹它的意義在于,它展示了一些很不一樣的東西。不過,這并不是說這個模式?jīng)]有從其他模 式中借鑒經(jīng)驗教訓:像是綁定、響應式編程、領域?qū)S谜Z言以及 reducer 都是已經(jīng)被熟知的想 法了。
歷史
MAVB 是 Matt Gallagher 在 Cocoa with Love 網(wǎng)站上首先提出的。這個模式參照了 Cocoa 綁 定、函數(shù)式響應動畫、ComponentKit、XAML、Redux 以及成千上萬行的使用 Cocoa view controller 的經(jīng)驗。
本書中的實現(xiàn)使用了 CwlViews 框架來處理 view 構(gòu)建、綁定器和適配器的實現(xiàn)等工作。
5. Elm 架構(gòu) (TEA)
TEA 和 MVC 有著根本上的不同。在 TEA 中,model 和所有的 view state 被集成為一個單個狀 態(tài)對象,所有 app 中的變化都通過向狀態(tài)對象發(fā)送消息來發(fā)生,一個叫做 reducer 的狀態(tài)更新 函數(shù)負責處理這些消息。
在 TEA 中,每個狀態(tài)的改變會生成一個新的虛擬 view 層級,它由輕量級的結(jié)構(gòu)體組成,描述 了 view 層級應該看上去的形式。虛擬 view 層級讓我們能夠使用純函數(shù)的方式來寫 view 部分 的代碼;虛擬 view 層級總是直接從狀態(tài)進行計算,中間不會有任何副作用。當狀態(tài)發(fā)生改變 時,我們使用同樣的函數(shù)重新計算 view 層級,而不是直接去改變 view 層級。
Driver 類型 (這是 TEA 框架中的一部分,它負責持有對 TEA 中其他層的引用) 將對虛擬 view 層 級和 UIView 層級進行比較,并且對它進行必要的更改,讓 view 和它們的虛擬版本相符合。這 個 TEA 框架中的 driver (驅(qū)動) 部件是隨著我們 app 的啟動而被初始化的,它自身并不知道要對 應哪個特定的 app。我們要在它的初始化方法中傳入這些信息:包括 app 的初始狀態(tài),一個通 過消息更新狀態(tài)的函數(shù),一個根據(jù)給定狀態(tài)渲染虛擬 view 層級的函數(shù),以及一個根據(jù)給定狀態(tài) 計算通知訂閱的函數(shù) (比如,我們可以訂閱某個 model store 更改時所發(fā)出的通知)。
從框架的使用者的視?來看,TEA 的關(guān)于更改部分的框圖是這樣的:
如果我們追蹤這張圖表的上面兩層,我們會發(fā)現(xiàn)在 view 和 model 之間存在我們在本章開頭是 就說過的反饋回路;這是一個從 view 到狀態(tài),然后再返回 view 的回路 (通過 TEA 框架進行協(xié) 調(diào))。
下面的回路代表的是 TEA中處理副作用的方式 (比如將數(shù)據(jù)寫入磁盤中):當在狀態(tài)更新方法中 處理消息時,我們可以返回一個命令,這些命令會被 driver 所執(zhí)行。在我們的例子中,最重要 的命令是更改 store 中的內(nèi)容,store 反過來又被 driver 所持有的訂閱者監(jiān)聽。這些訂閱者可 以觸發(fā)消息來改變狀態(tài),狀態(tài)最終觸發(fā) view 的重新渲染作為響應。
這些事件回路的結(jié)構(gòu)讓 TEA 成為了遵守單向數(shù)據(jù)流原則的設計模式的另一個例子。
1.構(gòu)建
狀態(tài)在啟動時被構(gòu)建,并傳遞給運行時系統(tǒng) (也就是 driver)。運行時系統(tǒng)擁有狀態(tài),store 是一 個單例。
初始的 view 層級和之后更新時的 view 層級是通過同樣的路徑構(gòu)建的:通過當前的狀態(tài),計算 出虛擬 view 層級,運行時系統(tǒng)負責更新真實的 view 層級,讓它與虛擬 view 層級相匹配。
2.更改 Model
虛擬 view 擁有與它們所關(guān)聯(lián)的消息,這些消息在一個 view 事件發(fā)生時會被發(fā)送。Driver 可以 接收這些消息,并使用更新方法來改變狀態(tài)。更新方法可以返回一個命令 (副作用),比如我們
想在 store 中進行的改動。Driver 會截獲該命令并執(zhí)行它。TEA 讓 view 不可能直接對狀態(tài)或者 store 進行更改。
3.更改 View
運行時系統(tǒng)負責這件事。改變 view 的唯一方式是改變狀態(tài)。所以,初始化創(chuàng)建 view 層級和更
新 view 層級之間沒有區(qū)別。4. View State
View state 是包含在整體的狀態(tài)之中的。由于 view 是直接從狀態(tài)中計算出來的,導航和交互狀 態(tài)也同樣會被自動更新。
4.測試
在大多數(shù)架構(gòu)中,讓測試部件彼此相連往往要花費大量努力。在 TEA 中,我們不需要對此進行 測試,因為 driver 會自動處理這部分內(nèi)容。類似地,我們不需要測試當狀態(tài)變化時 view 會正確 隨之變化。我們所需要測試的僅僅是對于給定的狀態(tài),虛擬 view 層級可以被正確計算。
要測試狀態(tài)的變更,我們可以創(chuàng)建一個給定的狀態(tài),然后使用 update 方法和對應的消息來改 變狀態(tài)。然后通過對比之前和之后的狀態(tài),我們就可以驗證 update 是否對給定的狀態(tài)和消息 返回了所期望的結(jié)果。在 TEA 中,我們還可以測試對應給定狀態(tài)的訂閱是不是正確。和 view 層級一樣,update 函數(shù)和訂閱也都是純函數(shù)。
因為所有的部件 (計算虛擬 view 層級,更新函數(shù)和訂閱) 都是純函數(shù),我們可以對它們進行完 全隔離的測試。任何框架部件的初始化都是不需要的,我們只用將參數(shù)傳遞進去,然后驗證結(jié) 果就行了。我們 TEA 實現(xiàn)中的大多數(shù)測試都非常直截了當。
Elm 架構(gòu)的重要性
TEA 最早是在 Elm 這?函數(shù)式語言中被實現(xiàn)的。所以 TEA 是一種如何用函數(shù)式的方法表達 GUI 編程的嘗試。TEA 同時也是最為古老的單向數(shù)據(jù)流架構(gòu)。
歷史
Elm 是 Evan Czaplicki 所設計的函數(shù)式編程語言,它最初的目的是為了構(gòu)建前端 web app。TEA 是歸功于 Elm 社區(qū)的一個模式,它的出現(xiàn)是語言約束和目標環(huán)境相互作用的自然結(jié)果。它 背后的思想影響了很多其他的基于 web 的框架,其中包括 React、Redux 和 Flux 等。在 Swift 中,還沒有 TEA 的權(quán)威實現(xiàn),不過我們可以找到不少研究型的項目。在本書中,我們使用 Swift 按我們自己的理解實現(xiàn)了這個模式。主要的工作由 Chris Eidhof 于 2017 年完成。雖然我 們的這個實現(xiàn)還并不是 “產(chǎn)品級” 的,但是許多想法是可以用在生產(chǎn)代碼中的。
三,其他APP架構(gòu)模式
1. Model-View-Presenter
Model-View-Presenter (MVP) 是一種在 Android 上很流行的模式,在 iOS 中,也有相應的實 現(xiàn)。在總體結(jié)構(gòu)和使用的技術(shù)上,它粗略來說是一種位于標準 MVC 和 MVVM 之間的模式。
MVP 使用單獨的 presenter 對象,它和 MVVM 中 view-model 所扮演的?色一樣。相對 view-model 而言,presenter 去除了響應式編程的部分,而是把要展示的值暴露為接口上的屬 性。不過,每當這些值需要變更的時候,presenter 會立即將它們推送到下面的 view 中去 (view 將自己作為協(xié)議暴露給 presenter)。
從抽象的觀點來看,MVP 和 MVC 很像。Cocoa 的 MVC,除了名字以外,就是一個 MVP - 它是 從上世紀九十年代 Taligent 的原始的 MVP 實現(xiàn)中派生出來的。View,狀態(tài)和關(guān)聯(lián)的邏輯在兩 個模式中都是一樣的。不同之處在于,現(xiàn)代的 MVP 中有一個分離的 presenter 實體,它使用協(xié) 議來在 presenter 和 view controller 之間進行界定,Cocoa 的 MVC 讓 controller 能夠直接引 用 view,而 MVP 中的 presenter 只能知道 view 的協(xié)議。
有些開發(fā)者認為協(xié)議的分離對于測試是必要的。當我們在討論測試時,我們會看到標準的 MVC 在沒有任何分離的情況下,也可以被完整測試。所以,我們感覺 MVP 并沒有太大不同。如果我 們對測試一個完全解耦的展示層有強烈需求的話,我們認為 MVVM 的方式更簡單一些:讓 view controller 通過觀察去從 view-model 中拉取值,而不是讓 presenter 將值推送到一個協(xié) 議中去。
2. VIPER,Riblets,和其他 “Clean” 架構(gòu)
VIPER,Riblets 和其他類似的模式嘗試將 Robert Martin 的 “Clean Architecture” 從 web app 帶到 iOS 開發(fā)中,它們主要把 controller 的職責分散到三到四個不同的類中,并用嚴格的順序 將它們排列起來。在序列中的每個類都不允許直接引用序列中前面的類。
為了強制單方向的引用這一規(guī)則,這些模式需要非常多的協(xié)議,類,以及在不同層中傳遞數(shù)據(jù) 的方式。由于這個原因,很多使用這些模式的開發(fā)者會去使用代碼生成器。我們的感覺是,這 些代碼生成器,以及任何的繁雜到需要生成器的模式,都產(chǎn)生了一些誤導。將 “Clean” 架構(gòu)帶 到 Cocoa 的嘗試通常都宣稱它們可以管理 view controller 的 “肥大化” 問題,但是讓人啼笑皆 非的是,這么做往往讓代碼庫變得更大。
雖然將接口分解是控制代碼尺寸的一種有效手段,但是我們認為這應該按需進行,而不是教條 式地對每個 view controller 都這么操作。分解接口需要我們對數(shù)據(jù)以及所涉及到的任務有清楚 的認識,只有這樣,我們才能達到最優(yōu)的抽象,并在最大程度上降低代碼的復雜度。
3. 基于組件的架構(gòu) (React Native)
如果你選擇使用 JavaScript 而不是 Swift 編程,或者你的 app 重度依賴于 web API 的交互, JavaScript 會是更好的選擇,這時你可能會考慮 React Native。不過,本書是專注于 Swift 和 Cocoa 的,所以我們將探索模式的界限定在了這些領域內(nèi)。
如果你想要找一些類似 React Native,但是是基于 Swift 的東西的話,可以看看我們對 TEA 的 探索。MAVB 的實現(xiàn)也從 ComponentKit 中獲得了一些啟發(fā),而 ComponentKit 本身又從 React 中獲取靈感:它使用類 DSL 的語法來進行聲明式和可變形的 view 構(gòu)建,這和 React 中 Component 的 render 方法及其相似。
審核編輯:湯梓紅
-
Apple
+關(guān)注
關(guān)注
1文章
929瀏覽量
52799 -
APP
+關(guān)注
關(guān)注
33文章
1573瀏覽量
72482 -
iOS
+關(guān)注
關(guān)注
8文章
3395瀏覽量
150604 -
MVC
+關(guān)注
關(guān)注
0文章
73瀏覽量
13857 -
架構(gòu)設計
+關(guān)注
關(guān)注
0文章
31瀏覽量
6929
原文標題:iOS APP 架構(gòu)設計
文章出處:【微信號:AndroidPush,微信公眾號:Android編程精選】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
評論