概述
傻瓜圖解
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)的工作也搶過來干。