iOS架構(gòu)補(bǔ)完計(jì)劃--淺談MVC及其衍生架構(gòu)模式(附簡易圖解)

目錄

概述

傻瓜圖解

MVC

一個正統(tǒng)的MVC、三者的任務(wù)是什么?

關(guān)于View到底該不該寫一些業(yè)務(wù)代碼

胖Model與瘦Model

強(qiáng)業(yè)務(wù)、弱業(yè)務(wù)

胖Model

瘦Model

該用哪個?

MVVM

Model

View

ViewModel

Controller

ReactiveCocoa對于MVVM的意義是什么?

MVCS

MVP

VIPER

關(guān)于架構(gòu)設(shè)計(jì)、一些觀點(diǎn)

控制好Controller的代碼量

對于MVX如何選擇

無論用哪種模式、都要深刻的理解每個模塊不同的職責(zé)

概述

其實(shí)只要是架構(gòu)上的設(shè)計(jì)、本質(zhì)上都是三個角色:數(shù)據(jù)管理者、數(shù)據(jù)加工者、數(shù)據(jù)展示者。

不管是MVC、MVVM、MVP、VIPER或者任何新的設(shè)計(jì)模式、都跳不出這三個角色。無非是把數(shù)據(jù)管理者的工作進(jìn)行拆分、唯一的界定標(biāo)準(zhǔn)就是把工作拆分的粒度大小。

而無論哪種思想、最終都逃不開三個問題的取舍。代碼量、通用性、可讀性。

這里我主要寫的是MVC和MVVM、對于其他的架構(gòu)只是略微提及。

傻瓜圖解

這兩天總有人跟我說我想看Demo...我想了想覺得Demo其實(shí)也不太直觀、干脆畫幾個圖好了。

主要是想表達(dá)每個模塊里應(yīng)該放什么類型的東西、線可能有連得不對的地兒。親們盡量意會吧。

MVC

MVC--胖Model

MVC--瘦Model

MVC--胖瘦結(jié)合

MVVM

MVP

VIPER

試了試...感覺畫著費(fèi)勁。能理解了前面幾個的話、看文字應(yīng)該也能理解VIPER吧。

MVC

MVC就是典型的著重通用型與可讀性、這正是一個作為萬物之初的架構(gòu)所需要保證的事。簡單、易學(xué)。

Model進(jìn)行數(shù)據(jù)管理

View進(jìn)行數(shù)據(jù)展示

Controller負(fù)責(zé)根據(jù)需求對Model以及View進(jìn)行調(diào)配。

不過和廣義的MVC不同、客戶端(別的我不知道啊、起碼iOS)由于UIViewController自帶一個容器View、所以除了上述的正統(tǒng)任務(wù)之外、Controller還需要承擔(dān)View的生成、布局等的任務(wù)。

一個正統(tǒng)的MVC、三者的任務(wù)是什么?

所以、我們可以將MVC三者的任務(wù)再進(jìn)一步細(xì)化一下

Model:

為Controller的讀取提供數(shù)據(jù)

為Controller的寫入提供接口

為Controller提供基本的業(yè)務(wù)組件

最常用的就是網(wǎng)絡(luò)請求之后Json轉(zhuǎn)Model、寫入數(shù)據(jù)庫之前Model轉(zhuǎn)Json。

View:

界面的展示

響應(yīng)與業(yè)務(wù)無關(guān)的事件(動畫效果、點(diǎn)擊反饋、點(diǎn)擊事件的開關(guān)保護(hù)等等)

何為業(yè)務(wù)事件:Model數(shù)據(jù)的改變、網(wǎng)絡(luò)請求的發(fā)送、頁面的跳轉(zhuǎn)、頁面的刷新等等。

Controller

管理self.view的生命周期

負(fù)責(zé)生成所有的View實(shí)例、以及布局。

將恰當(dāng)?shù)腗odel交付給View展示。

監(jiān)聽來自View與業(yè)務(wù)有關(guān)的事件、通過與Model的合作、來完成對應(yīng)事件的業(yè)務(wù)。

關(guān)于View到底該不該寫一些業(yè)務(wù)代碼

其實(shí)自己以前。也會圖方便、把一些自認(rèn)為的弱業(yè)務(wù)寫在View里。

舉個例子:

一個Cell、有用戶Nickname、還有一個用戶頭像的Button。

點(diǎn)擊事件肯定由Cell捕獲。這個時(shí)候跳轉(zhuǎn)用戶主頁的動作、該由View完成、還是傳遞給Controller?

假設(shè)我們交由View、也就是當(dāng)前的Cell跳轉(zhuǎn)了。很方便、省去了寫代理的小十行代碼。

并且這個Cell也可以挪到其他頁面去使用、一樣能跳到用戶主頁、又省去不少代碼。

有一天、產(chǎn)品讓你在某個頁面點(diǎn)擊頭像不執(zhí)行任何動作。

咋辦呢?機(jī)智如你、給這個Cell添加了一個bool值來控制是否跳轉(zhuǎn)就好了。

又過了幾天、產(chǎn)品讓你在某個頁面把這個頭像弄成點(diǎn)擊之后彈出舉報(bào)框。

這怎么搞呢?也不是不行、你又給這個Cell添了一個枚舉的type。這簡直完美、不同的Type執(zhí)行不同的事件、你順便取消了那個bool、把他也寫成了一個type。

又過了一陣子、產(chǎn)品又告訴你當(dāng)滿足某些條件的時(shí)候、這個頭像跳主頁。另一些條件的時(shí)候、這個頭像不能跳。

于是、你終于寫了個block或者代理、在點(diǎn)擊之后執(zhí)行一下再看下一步怎么跳轉(zhuǎn)。

此時(shí)、回過頭來再看你的Cell、已經(jīng)面目全非了、充斥著各種業(yè)務(wù)判斷。

可能你會說、如果產(chǎn)品真的這么二逼。那我干脆再copy一個Cell就好了啊。

但是別忘了、你當(dāng)初這么設(shè)計(jì)這個Cell的時(shí)候可是為了節(jié)省下頁面跳轉(zhuǎn)的小十行代碼、而你現(xiàn)在卻要為此付出copy一整個Cell的代價(jià)。

其實(shí)還有一個更重要的問題、就是你這個View的模塊復(fù)用基本為0

假設(shè)你需要另起一個新的工程寫一個demo、如果用這個View你首先要解決一大堆跳轉(zhuǎn)代碼上Controller 文件的缺失、然后還會發(fā)現(xiàn)、原來寫的很多邏輯、type在這個demo里毫無用處。挨個刪除、梳理邏輯又要耗費(fèi)很多時(shí)間。

而這些將來會發(fā)生的問題、如果你最開始不把業(yè)務(wù)事件代碼硬寫進(jìn)View里、一件都不會發(fā)生。

胖Model與瘦Model

這里先要引出兩個概念。強(qiáng)業(yè)務(wù)、弱業(yè)務(wù)。

二者關(guān)鍵的區(qū)別是代碼變動的頻率大小與涉及模塊的多少。舉兩個例子:

1、比如把時(shí)間戳轉(zhuǎn)化、小數(shù)點(diǎn)的格式化或者修改A屬性進(jìn)行一系列計(jì)算并且改變B屬性、這種業(yè)務(wù)就屬于弱業(yè)務(wù)。

2、再比如一個一個訂單Model的確認(rèn)收貨、就應(yīng)該歸入沒辦法歸入弱業(yè)務(wù)、因?yàn)樯婕熬W(wǎng)絡(luò)請求、加密等等多個底層模塊。

胖Model

主旨是Controller從Model里拿到的數(shù)據(jù)、不需要進(jìn)行更多的判斷、處理等操作、就能使用。舉個例子:

Raw Data:? ? timestamp:1234567FatModel:@property(nonatomic,assign)CGFloattimestamp;? ? - (NSString*)ymdDateString;// 2015-04-20 15:16- (NSString*)gapString;// 3分鐘前、1小時(shí)前、一天前、2015-3-13 12:34Controller:self.dateLabel.text = [FatModel ymdDateString];self.gapLabel.text = [FatModel gapString];

這就需要將弱業(yè)務(wù)、寫進(jìn)Model、很好的滿足的復(fù)用的需求。

胖Model也是存在問題的、就是移植的困難。畢竟業(yè)務(wù)再弱、也是代碼、當(dāng)項(xiàng)目成長到一定程度、這個Model也將會變得相當(dāng)?shù)挠纺[。

瘦Model

就是要把MVC的M貫徹倒底、除了業(yè)務(wù)的表達(dá)啥都不管。

但是這樣又會導(dǎo)致Controller中的代碼變得異常臃腫(廢話么、連時(shí)間戳轉(zhuǎn)化都要交給Controller不腫才怪)

所以瘦Model要借助一些外來的輔助模塊(索性可以叫Helper)來對弱業(yè)務(wù)做抽象。舉個例子:

Raw Data:{"name":"casa","sex":"male",}SlimModel:@property(nonatomic,strong)NSString*name;@property(nonatomic,strong)NSString*sex;Helper:#define Male 1;#define Female 0;+ (BOOL)sexWithString:(NSString*)sex;Controller:if([Helper sexWithString:SlimModel.sex] == Male) {? ? ? ? ...? ? }

該用哪個?

我個人用胖Model用的比較多、但是也借助了一些瘦Model的思想。舉例來講:

除了上述很明確的可以放到Model里的弱業(yè)務(wù)之外、像一個訂單中的確認(rèn)收貨、發(fā)貨、申請退貨等等操作、他們既不算特別強(qiáng)的業(yè)務(wù)、而且還有很高的復(fù)用需求(訂單列表和訂單詳情都需要確認(rèn)收貨)。

這種業(yè)務(wù)有一種特點(diǎn)、就是代碼就在哪里。不管你放到哪、都只能挪不能刪。但挪到哪、都不完全合適。從定義上來講十分莫若兩可。個人覺得:

這種的業(yè)務(wù):能不放在Controller里就不要放

你可以干脆放到胖Model里、畢竟將來拆分一個400行的Model、比拆分一個1400行的Controller容易得多。

你也可以單獨(dú)新建一個Helper、配合著Model來完成業(yè)務(wù)。這樣想移植頁面就單用Model、想帶業(yè)務(wù)移植就帶著Helper(其實(shí)這個思路已經(jīng)很接近MVP了、但是還差提點(diǎn)。MVP還需要為View提供數(shù)據(jù))。

MVVM

弱弱的一說、我并不推薦iOS中寫MVVM(因?yàn)槿肭中詫?shí)在太強(qiáng))、不會教你怎么用RAC怎么寫出MVVM、只是想讓你理解什么是MVVM

MVVM現(xiàn)在已經(jīng)是一種非常成熟的思想了。應(yīng)用也十分普及、例如Vue以及小程序。

MVVM的初衷也是為了Controller減負(fù)。

剛才的胖Model只從Controller移植走了一些簡單的弱業(yè)務(wù)。

而ViewModel則干脆把數(shù)據(jù)的處理全部從Controller移植了出去。

理想上相同的輸入(比如網(wǎng)絡(luò)服務(wù)響應(yīng))將會導(dǎo)出相同的輸出(屬性的值)。

簡單的說一下M、V、VM的在架構(gòu)中所扮演的角色。

Model:

和正統(tǒng)MVC中的瘦Model一樣、只承載最基本的數(shù)據(jù)單元。

@interfaceUserListModel:NSObject@property(nonatomic,strong,readonly)NSString*userName;@property(nonatomic,strong,readonly)UIImage*portraitImg;@end

View

其實(shí)也和正統(tǒng)的MVC一樣、只做展示工作、不承接任何業(yè)務(wù)邏輯。

但是需要注意的是、有時(shí)候也會在View中將ViewModel與View做一些綁定工作(ViewModel本質(zhì)上也算是Model層、所以View并不適合直接持有ViewModel)。

- (void) awakeFromNib {? ? [superawakeFromNib];? ? RAC(self. portraitImgView,? image) = RACObserve(self,? viewModel. portraitImg);? ? RAC(self. userNameLabel,? text) = RACObserve(self,? viewModel. userName);}

ViewModel

提供了這個頁面展示所有需要的數(shù)據(jù)的一個對象。

舉一個簡單的例子:

@interfaceUserListViewModel:NSObject@property(nonatomic,assign,readonly)BOOLloading;@property(nonatomic,strong,readonly)NSArray*userList;@property(nonatomic,strong,readwrite)NSString*searchUserName;? - (void) searchUser;- (void) deleteUserWithModel:(UserListModel *)model;- (void) loadMoreUser;

這個ViewModel里涵蓋了所有頁面展示需要的要素。用戶列表、搜索名稱、是否需要顯示網(wǎng)絡(luò)加載的小菊花。

并且涵蓋了對這些數(shù)據(jù)的所有操作方法。加載更多、搜索、刪除。

但是、ViewModel到底也是一個Model層、不應(yīng)該引入U(xiǎn)IKit(View層)。如果刪除需要彈窗、那么這個彈窗動作是不應(yīng)該交給ViewModel來搞的、因?yàn)檫@已經(jīng)不屬于數(shù)據(jù)處理的范疇了

仔細(xì)想想、這個ViewModel其實(shí)就是把Controller中與頁面相關(guān)的數(shù)據(jù)處理代碼挪進(jìn)來了而已。

如此、我們設(shè)置可以脫離View層。拿著這個ViewModel去跑單元測試。簡直碉堡。

Controller

雖然MVVM中沒有體現(xiàn)出C的字眼、但是實(shí)際操作肯定是要遵循著View <-> C <-> ViewModel <-> Model。

起碼在iOS中是、這和Vue中簡單粗暴的方式不同:

//頁面里

{{message}}

? ? ? {{value}}

//js文件里newVue({//數(shù)據(jù)data:{? ? ? ? ? ? key:'welcome vue',? ? ? ? ? ? arr:['apple','banana','orange','pear'],? ? ? ? ? ? json:{a:'apple',b:'banana',c:'orange'}? ? ? }//方法methods:{? ? ? ? ? ? add:function(){//push 添加元素this.arr.push('tomato');? ? ? ? ? ? }? ? ? }})

因?yàn)镠tml中并沒有明確的Controller的概念、整個Html文件就是Controller容器。

和iOS的區(qū)別很明顯:

除去精煉的寫法、整個Html文件所關(guān)聯(lián)的js資源都可以無障礙互通、所以View層無時(shí)無刻不持有著Model層、在View層直接綁定更方便。

所以iOS中Controller的作用就顯而易見了

Controller夾在View和ViewModel之間做的其中一個主要事情就是將View和ViewModel進(jìn)行綁定。在邏輯上、Controller知道應(yīng)當(dāng)展示哪個View、Controller也知道應(yīng)當(dāng)使用哪個ViewModel、然而View和ViewModel它們之間是互相不知道的、所以Controller就負(fù)責(zé)控制他們的綁定關(guān)系。

ReactiveCocoa對于MVVM的意義是什么?

ReactiveCocoa并不是MVVM思想的根本、不用ReactiveCocoa也能MVVM、用ReactiveCocoa能更好地體現(xiàn)MVVM的精髓。

我一直強(qiáng)調(diào)MVC中的M與V是應(yīng)該盡量不要互相持有的。

這個時(shí)候如何把原本松散的二者通過C緊密的聯(lián)系起來、就要進(jìn)行數(shù)據(jù)綁定。

而這種數(shù)據(jù)綁定、iOS本身并沒有什么太靠譜的辦法(就像剛才前端例子中的

{{message}}

、這種寫法)。

雖然KVO、Notification、block、delegate和target-action都可以用來做數(shù)據(jù)通信進(jìn)而實(shí)現(xiàn)綁定

但都不如ReactiveCocoa來的《《《優(yōu)雅》》》。

對、這就是我開始說為什么RAC對于MVVM不是必須的。

如果不用ReactiveCocoa、綁定關(guān)系可能就做不到那么松散那么好、但并不影響它還是MVVM。

MVCS

將數(shù)據(jù)持久化的代碼移植給了store...

MVP

實(shí)際上就是將Controller中關(guān)于Model與View的調(diào)配處理的代碼移植了過來。各部分分工如下:

View

負(fù)責(zé)界面展示和布局管理、向Presenter暴露視圖更新和數(shù)據(jù)獲取的接口

Presenter

負(fù)責(zé)接收來自View的事件、通過View提供的接口更新視圖,并管理Model。

Model

和MVC中的一樣,提供數(shù)據(jù)模型

VIPER

除了View沒拆、其它的都拆了....

在MVP的基礎(chǔ)上新增了Interactor與Router

View

提供完整的視圖。負(fù)責(zé)視圖的組合、布局、更新

向Presenter提供更新視圖的接口

將View相關(guān)的事件發(fā)送給Presenter

Interactor

維護(hù)主要的業(yè)務(wù)邏輯功能,向Presenter提供現(xiàn)有的業(yè)務(wù)用例

維護(hù)、獲取、更新Entity

當(dāng)有業(yè)務(wù)相關(guān)的事件發(fā)生時(shí)、處理事件、并通知Presenter

Presenter

接收并處理來自View的事件

向Interactor請求調(diào)用業(yè)務(wù)邏輯

向Interactor提供View中的數(shù)據(jù)

接收并處理來自Interactor的數(shù)據(jù)回調(diào)事件

通知View進(jìn)行更新操作

通過Router跳轉(zhuǎn)到其他View

Entity

和Model一樣的數(shù)據(jù)模型

Router

提供View之間的跳轉(zhuǎn)功能、減少了模塊間的耦合

初始化VIPER的各個模塊

VIPER與其他的架構(gòu)相比最大的優(yōu)勢就是粒度簡直細(xì)化成了塵埃、極大的提高了可測性。

但問題也相當(dāng)顯著、層級越多、數(shù)據(jù)傳遞的工作量(API)就越大。文件越多、新建一個頁面的成本也就越高。

關(guān)于架構(gòu)設(shè)計(jì)、一些觀點(diǎn)

除了MVVM、它對iOS的入侵性簡直太高、主要取決于團(tuán)隊(duì)的決策(比如是不是新項(xiàng)目、Leader是不是想玩玩看)。

控制好Controller的代碼量

隨著項(xiàng)目的進(jìn)行、代碼量最多只能優(yōu)化、膨脹不可避免。

而在沒辦法繼續(xù)精簡的前提下、想控制Controller的代碼量。就要在可讀性和通用性之間進(jìn)行取舍。該挪走的時(shí)候就挪走吧、畢竟梳理一個單獨(dú)的模塊、比梳理一個幾千行的Controller要方便多了。

對于MVX如何選擇

其實(shí)完全要看業(yè)務(wù)的性質(zhì)以及復(fù)雜度。

如果你一個頁面只有一個UITableView、搞出一些奇淫技巧其實(shí)意義? 不大、徒增煩惱。踏踏實(shí)實(shí)用MVC對大家都好。

如果感覺業(yè)務(wù)里有非常多的View與Model互通、或者需大量復(fù)用、可以用MVP。

如果有大量的數(shù)據(jù)讀寫、可以用MVCS。

如果業(yè)務(wù)相當(dāng)?shù)膹?fù)雜、耦合讓人渾身難受。做好模塊化或者干脆VIPER才是出路。

無論用哪種模式、都要深刻的理解每個模塊不同的職責(zé)

比如MVVM里的VM、既然是Model層、就不要把UIKit放進(jìn)去。

再比如MVP中的P、既然是為了幫助Controller協(xié)調(diào)M與V而生、就不要把與二者無關(guān)的工作也搶過來干。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容