設計模式

常用的面向對象設計方法和設計模式

MVC?

Model:表示業務數據對象;View:展現數據的 UI;Controller:Model 跟 View 之間的粘合劑。一方面對 View 上的行為作出反應,通常會涉及到 Model 的更改;

另一方面將 Model 的改動反映到 View 上。由于 Controller 作為粘合劑的存在,View 和 Model 只需要跟 Controller 交互,而不知道另一方的存在。

這樣,View 和 Model 作為獨立可復用的組件,Controller 里處理業務邏輯。聽起來這樣的架構很清晰直觀,實際應用中,MVC 對于不是很復雜的 App 也是非常高效的。

但對稍復雜些的 App,MVC 使用起來就會非常吃力。你可能聽過 MVC 也被簡稱為 Massive View Controller,這就是原因所在 。

View Controller 承擔的職責太多:網絡請求、數據訪問和存儲、UI 的調整和組合、業務邏輯、View 的 delegate、data source、狀態的維護與單一責任準則(Single Responsibility Principle)背道而馳。

過于臃腫的 View Controller 使 App 的維護成本非常高。盡管把網絡請求以及數據訪問和存儲放到了 Model 里,但由于對象邊界的定義不夠清晰,大部分 View Controller 依然很臃腫,上千行的 View Controller 很常見。

關于 View Controller 有個準則:如果一個 View Controller 超過了 300 行代碼,那它一定做了責任范圍以外的事。

更不幸的是由于一些職責移交給 Model,導致 Model 也變得臃腫起來。原來唯一可以做 Unit Test 的 Model 現在測試也很困難。

為解決 Massive View Controller 的問題,MVVM、VIPER 等架構應運而生。要發揮 MVVM 的優勢,需要有 Reactive。Reactive 增加學習成本的同時,也讓調試變得更困難。

VIPER雖然能平衡責任的分配,但由于引入過多對象,維護成本高。一個簡單的頁面也要求新增多個類和大量傻瓜代碼。

MVVM ?

MVVM 架構是 MV(X) 里面最新的一個,它的出現已經考慮到了 MV(X) 模式之前所遇到的問題。

理論上來說,Model - View - ViewModel 看起來非常棒。View 和 Model 已經很熟悉了,中間人的角色VC控制器也很熟悉了,但是在MVVM架構里中間人的角色由VC控制器變成了ViewModel

iOS 里面的 ViewModel 到底是個什么東西呢?ViewModel能持有Model進而修改Model,同時能夠隨時監聽Model的屬性數據變化進而來開啟事件,因為ViewModel同時持有View,所以能夠在監聽Model變化開啟的事件里直接對View進行更新。

綁定View和ViewModel的方法包括:

1、使用基于 KVO 的綁定庫,比如 RZDataBinding 或者 SwiftBond。
2、使用全量級的函數式響應編程框架,比如ReactiveCocoa、RxSwift 或者 PromiseKit。

實際上,現在提到「MVVM」你應該就會想到 ReactiveCocoa,反過來也是一樣。

雖然我們可以通過簡單的綁定來實現 MVVM 模式,但是 ReactiveCocoa(或者同類型的框架)會讓你更大限度的去理解 MVVM。

當然響應式編程框架(FRF)也有一點不好的地方負責的地方就是學習曲線陡峭,如果半懂不懂很容易讓事情更糟。什么地方出錯需要花費更多的時間去調試。

MVP?

MVP(被動變化的 View),在 MVC 里面 View 和 Controller 是耦合緊密的,但對 MVP 里面的 Presenter 來講,它根本不關注 ViewController 的生命周期,而且 View 也能被簡單 mock 出來,所以在 Presenter 里面基本沒什么布局相關的代碼,它的職責只是通過數據和狀態更新 View。

在 MVP 架構里面,UIViewController 的那些子類其實是屬于 View 的,而不是 Presenter。

這種區別提供了極好的可測性,但是這是用開發速度的代價換來的,因為你必須要手動的去創建數據和綁定事件。

MVP 架構擁有三個真正獨立的分層,所以在組裝的時候會有一些問題,而 MVP 也成了第一個披露了這種問題的架構。

因為我們不想讓 View 知道 Model 的信息,所以在當前的 ViewController(角色其實是 View)里面去進行組裝肯定是不正確的,我們應該在另外的地方完成組裝。

比如,我們可以創建一個應用層(app-wide)的 Router 服務,讓它來負責組裝和 View-to-View 的轉場。

這個問題不僅在 MVP 中存在,在接下來要介紹的模式里面也都有這個問題。

MVP把大部分的職責都分配到了 Presenter 和 Model 里面,而 View 基本上不需要做什么(在上面的例子里面,Model 也什么都沒做)

可測性 - 簡直棒,我們可以通過 View 來測試大部分的業務邏輯。

易用 - 就我們上面那個簡單的例子來講,代碼量差不多是 MVC 架構的兩倍,但是 MVP 的思路還是蠻清晰的。

MVP 架構在 iOS 中意味著極好的可測性和巨大的代碼量。

還存在著另一種的 MVP - Supervising Controller MVP。這個版本的 MVP 包括了 View 和 Model 的直接綁定,與此同時 Presenter(Supervising Controller)仍然繼續處理 View 上的用戶操作,控制 View 的顯示變化。

MVVM比較MVP?

  • 劃分性。MVVM 框架里面的 View 比 MVP 里面負責的事情要更多一些。MVVM是通過 ViewModel 的數據綁定來更新View的數據,而MVP只是把所有的事件統統交給 Presenter 去進行數據處理但并不負責更新View的數據,這也是MVP無法很好使用FRP響應式框架的原因。

  • 可測性。在MVVM中, ViewModel 對View 是一無所知的,而View則完全依賴ViewModel處理數據并釋放出處理結果的信號。因此MVVM的測試ViewModel里面的邏輯漏洞變得特別簡單。

  • 易用性。在實際的應用當中 MVVM 會更簡潔一些。在 MVP 下你必須要把 View 的所有事件都交給 Presenter 去處理,而且需要手動的去更新 View 的狀態;而在 MVVM 下,你只需要用使用KVO或是ReactiveCocoa綁定就可以解決。而且MVVM 所有框架中唯一自動更新視圖的框架,因為在 View 上已經做了數據綁定,只要ViewModel持有的Model屬性的特定屬性值發送變化,自動就會發送數據數據變化的信號,然后更新View上UI控件的值。

VIPER ?

VIPER框架不屬于任何一種 MV(X) 框架。

MV(X) 本質上是把模塊分成三層,X無論是什么,都是器管道紐帶連接的作用。

但是VIPER能夠具備更細的顆粒度,將模塊分成了五層。

1、Interactor(交互器) 處理數據(Entities)或網絡請求的業務邏輯。
2、Presenter(展示器) 處理UI相關的業務邏輯,可以調用 Interactor 中的方法來屬性數據。3、Entities(實體)作為一純粹的數據對象只是用來規范數據和記錄數據。
4、Router(路由)主要是聯系 VIPER各個模塊之間的紐帶。

MV(X)對比VIPER ?

  • 劃分性。Controller/Presenter/ViewModel 的職責里面,只有 UI 的展示功能被轉移到了 Presenter 里面。Presenter 不具備直接更改數據的能力。但是毫無疑問的,VIPER 在職責劃分方面是做的最好的,畢竟顆粒度分了五層。

  • 可測性。理所當然的,職責劃分的越好,測試起來就越容易。

  • 易用性。良好的職責劃分就意味著一個小小的任務,可能就需要你為各種類寫大量的接口,維護代價直線升高。

后臺架構?

服務器端開發常用Service Oriented Architecture服務導向架構,把業務分成了多個邏輯獨立的組件。

一個組件相當于一個 Service,封裝了與其業務相關的功能,如 UserService 負責用戶的注冊、登入等,而 BabyService 有 Baby 的增加、移除、以及數據的記錄等。

Service 是對整個架構縱向邏輯切分的結果。

拋開業務邏輯談 Service 意義不大,Service 通常與數據庫表的設計緊密相關。

橫向的邏輯切分將 Baby App iOS 的架構自上而下切分成三個層(Layer):應用層(Application Layer)、服務層(Service Layer)、數據層(Data Access Layer)。

服務層和數據層把復雜的邏輯封裝起來,作為 Framework 提供接口給上層調用。

應用層只能調用服務層暴露出來的接口,而不能直接調用數據層。

層次結構加強了可重用性和可測試性。應用層調用服務層提供的簡單接口獲得數據或者實行用戶操作。

服務層也不需要知道數據層中網絡請求,服務器同步,以及數據持久化的具體實現。

服務層,數據層,以及應用層都能很容易實現各自的單元測試(Unit Test)。Framework 是很棒的工具。把服務層和數據層打包成 Framework,不僅幫助構建解耦可重用的代碼,同時 App 的結構和業務邏輯也更加清晰。

應用層(Application Layer)應用層也可以叫展示層(Presentation Layer),負責 UI 跟 展示邏輯。

從Code角度說,就是 UIView 跟 UIViewController 的集合。

復雜的邏輯都封裝到了下層,UIViewController 就變得十分輕量。

View Controller主要負責三件事:

1、從 Service 獲得數據(ViewModel)并展示
2、響應用戶操作,調用相應的 Service 接口
3、監聽 Service 層發出的消息,并執行相應操作,如更新 UI。

從 Service 獲取的 ViewModel 實例并不是 NSManagedObject 或者其他持久化的 model 實例,跟 MVVM 中的 ViewModel 也不一樣。

它只是簡單的 Swift Struct,提供應用層需要的數據值。

使用 Struct 結構體的好處主要是:

1、值類型(Value Type): 簡單、容易理解,線程安全
2、松耦合,減少 View Controller 之間可能的交互
3、減少了 Statefulness 和 Mutability似的應用運行更高效、占用內存更少。

使用 Struct 也就意味著想要底層持久化 Model 的更改反映到 UI 上,你必須通過 Service 再抓一次數據。

也許有人認為這是使用 Struct 的一個缺點,其實不是,這應該是優點。因為 Immutable 的 ViewModel ,讓 View Controller 變得更加簡單,你不用擔心其他地方的代碼會更改你的 ViewModel 實例。

調試起來也會更加方便,代碼更容易理解、可讀更高。

WWDC 中有好幾個視頻都對 Struct 的使用和優勢進行了詳解。服務層(Service Layer)服務層定義了一系列 Service 和供給應用層使用的 ViewModel。

Service 封裝了 App 主要的業務邏輯,負責把底層持久化的 Model 和網絡請求返回的 JSON 轉換為 ViewModel,再提供給應用層使用。

這樣的分離即加強了 Immutablility 和 Statelessness,也讓應用層中的 ViewController 更輕量,只需幾行 Service calls。

Service 雖然承擔大部分業務邏輯,但一個 Service 通常也就 300 行左右的代碼量,這得益于數據層的封裝和抽象。

數據層(Data Access Layer)的作用是提供簡化的數據訪問接口,主要有 3 個模塊:數據存儲(Persistence)、網絡請求(Network)、數據同步(Data Synchronization)。

數據存儲我們使用的是 Core Data,也可以用 Realm 或者其他數據庫代替。

網絡請求我們使用了 Moya 進行抽象,使 API 的設計和調用更簡潔,并支持我們 Server 自定義的錯誤。數據同步模塊,會自動同步本地和服務器端的用戶數據。

MVC 因 Controller 的臃腫而遭到眾人詬病。但其實 MVC 作為最基礎的設計模式,展現了一個架構的精髓 - 抽象分離。

從整體看,數據層是 Model,業務層是 Controller,應用層是 View。

如果看細節的地方,應用層跟也務層提供的 ViewModel 也可以看做一個 MVC:ViewModel - UIViewController - UIView.

單例?

單例作為Cocoa框架中被廣泛使用的核心設計模式之一。事實上,蘋果開發者庫把單例作為 "Cocoa 核心競爭力" 之一。應用中我們經常和單例打交道,比如 UIApplication 和 NSFileManager 等等。我們在開源項目、蘋果示例代碼和 StackOverflow 中見過了無數使用單例的例子。但這也不可避免地產生了不好的影響:

1、全局狀態

使用全局可變的狀態使得程序變得難以理解,難以調試。因為在面向對象編程的一大基本原則就是,對于可變的變量狀態而言,作用域越小越能避免一個員工兩個老板的誤會。

2、隱式耦合

程序的任意模塊都可以訪問單例意味著任何和這個單例交互產生的副作用都會影響程序其他地方的任意代碼。這就表示即使是兩個完全獨立的模塊。也會因為單例提供的共享狀態而產生副作用影響。由于單例具有全局和多狀態的特性,導致隱式地在兩個看起來完全不相關的模塊之間建立了耦合。

3、單元測試

一個全局屬性變量的值長期存在不消失(持久化狀態)會嚴重干擾單元測試。比如說,A和B是兩個不同的模塊,但是都共同持有了一個單例對象的屬性值,這時候就不滿足單元測試的前提條件,兩個模塊相互獨立。

4、生命周期

在程序中添加一個單例時, “永遠只會有一個實例”的原則可能會被打破。例如賬號注銷用戶切換,舊用戶的存儲在全局單例之中的所有狀態數據都必須清理掉。同時也希望登錄的新用戶能夠使用一個全新的全局單例,而不是簡單的刪除單例的數據值。那么就必須在用戶注銷的時候置空舊用戶的單例類對象,新用戶登錄的時候實例化一個新的單例類對象。但是假如說現在用戶單例類正在存儲異步下載的圖片,如果突然置空舊用戶的單例類對象會造成繼續下載的圖片保留到了新用戶的單例里。所以必須保證在置空舊用戶的單例類對象之前將異步下載圖片的操作給關閉掉。但是問題就在于置空舊用戶的單例類對象時你很難準確地判斷出單例實例的所有者(因為單例自己管理自己的生命周期),準確判斷出“關閉”的單例是一個舊用戶的單例將變得非常的困難。

雜談?

雀圣和菜鳥正在觀看同一個人打麻將,雀圣能夠察覺到的內容會遠遠超過新手,并非雀圣火眼金睛,而是掌握了無形的武器,通過建立一整套思維抽象,雀圣能夠透過現象看到本質,把對原始現象的感知轉換成對目前局勢簡明扼要的理解。雀圣在看到牌局的一瞬間,就會聯想到某種進攻戰術的成功。這就叫觀察能力。

最好學些架構的方式就是先經過照貓畫虎式的時實踐,然后系統地培訓設計方法。

同一個應用里面,即便有幾種混合的架構模式也是很正常的一件事情。比如:開始的時候,你用的是 MVC 架構,后來你意識到有一個特殊的頁面用 MVC 做的的話維護起來會相當的麻煩;這個時候你可以只針對這一個頁面用 MVVM 模式去開發,對于之前那些用 MVC 就能正常工作的頁面,你完全沒有必要去重構它們,因為兩種架構是完全可以和睦共存的

MVVM 配合綁定機制效果最好,這個綁定機制就是RAC,使用MVC只能感受到RAC的部分好處。一說到綁定,自然就是想到了 KVO(Key-Value Observation)。然而,對于一個簡單的綁定都需要很大的樣板代碼,更不用說有許多屬性需要綁定了。所以ReactiveCocoa橫空出世,雖然MVVM 并未強制我們使用 ReactiveCocoa。但是MVVM 在良好的綁定框架下更能釋放出潛力。

模塊如何劃分?

桌面負責模擬控制臺和狀態顯示、嵌入式負責設備控制和狀態數據讀取開發技術如何選型
如何適應可能發生的變化、交互機制

如何設計程序?

看別人的設計成果,體會別人的設計過程,試著自己來設計。討論功能需求、討論非功能需求中的質量屬性需求、解決待完成任務的限制條件的約束需求。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 傳統模式下的開發MVCMVVM基于面向協議MVP的介紹MVP實戰開發說在前面:相信就算你是個iOS新手也應該聽說過...
    行走的菜譜閱讀 3,187評論 1 5
  • 一、概述 通過上一篇文章的學習,我們對關于MVC的弊端的產生和MVVM中viewModel的職責及其使用注意事項,...
    CoderMikeHe閱讀 28,438評論 110 352
  • 平樂和南憶走在白霆身后,仿佛已經看到了這個故事的結局。 “白霆,對不起,最終還是連累你了。”小乞忍著身上的痛有氣無...
    小哈兒閱讀 203評論 2 1
  • 南方的八月仍是盛夏,華氏九十多度,占地七百多畝的體育場路面人影稀寥,間或閃過也是打著低低的遮陽傘。 此刻正值晌午,...
    珂鳴閱讀 430評論 0 0
  • 大米,是我的好朋友,不是因為他長的像大米,而是因為他太愛吃大米了。 剛認識他的時候,我一度認為眼前這個瘦瘦高高的男...
    花枝招展的貓閱讀 147評論 0 0