前言
其實(shí)關(guān)于MVVM,筆者早就想談?wù)勛约旱南敕ㄟ罕笥褌兘涣鲗W(xué)習(xí)。但是由于這段時(shí)間公司任務(wù)緊理盆,加班多痘煤,而抽不出時(shí)間來。這樣一來離上一篇MVP模式已經(jīng)有兩個(gè)月了猿规。
起源
MVVM 最早于 2005 年被微軟的 WPF 和 Silverlight 的架構(gòu)師 John Gossman 提出衷快,并不斷完善。微軟為了MVVM姨俩,可謂是費(fèi)勁心力烦磁。為平臺(tái)整合了大量基礎(chǔ)設(shè)施和高級(jí)特性,如XAML哼勇、Blend、Bingding System呕乎、AttachBehavior积担、DependencyProperty等等,這些都大大的簡(jiǎn)化了MVVM的開發(fā)猬仁,使MVVM模式在微軟平臺(tái)得到了廣泛應(yīng)用帝璧。
MVVM在解決什么問題?
當(dāng)面試官問你這個(gè)問題的時(shí)候湿刽,千萬別只說MVVM是在給Controller瘦身的烁,這樣你一眼就被看穿對(duì)MVVM一知半解。任何一個(gè)模式的出現(xiàn)诈闺,一定為了解決軟件工程中某個(gè)特定的痛點(diǎn)渴庆,要么是為了提高開發(fā)效率,縮短軟件開發(fā)周期雅镊;要么是為了提高軟件的穩(wěn)定性襟雷、可擴(kuò)展性;要么是為了提高軟件的運(yùn)行性能等等仁烹,“瘦身”不過是被捎帶上的結(jié)果耸弄。那MVVM在解決什么問題呢?或者說MVVM為什么而出現(xiàn)呢卓缰?
MVVM是為了讓界面設(shè)計(jì)師專注于界面元素和交互计呈,從而能夠設(shè)計(jì)出讓用戶欣喜若狂的產(chǎn)品砰诵;讓開發(fā)者專注于邏輯,完全脫離UI捌显,從而能夠保證程序的穩(wěn)定性茁彭、可擴(kuò)展性、和高性能苇瓣。由于界面設(shè)計(jì)和業(yè)務(wù)完全分離尉间,使得更換界面變得簡(jiǎn)單,調(diào)整業(yè)務(wù)邏輯也不會(huì)對(duì)界面產(chǎn)生嚴(yán)重的影響击罪;另一方面哲嘲,也使得我們可以單獨(dú)對(duì)業(yè)務(wù)進(jìn)行單元測(cè)試。
模式解析
Model層:數(shù)據(jù)服務(wù)層媳禁,跟其他類MVC模式一樣眠副,不管最終對(duì)接的是數(shù)據(jù)庫還是網(wǎng)絡(luò)API或是其它,都是在負(fù)責(zé)數(shù)據(jù)的存儲(chǔ)竣稽,并提供訪問數(shù)據(jù)的接口囱怕,以支持?jǐn)?shù)據(jù)的增刪改查的基本操作。
View層:界面層毫别,但大家注意娃弓,這里的View層相對(duì)于MVP模式中的View來說,指代的范圍更狹小岛宦,原則上它不包含任何界面邏輯台丛,在基本組件庫滿足需求的情況下,View層的設(shè)計(jì)和制作完全不需要程序員的參與砾肺,所有工作都由界面設(shè)計(jì)師完成挽霉,這也是MVVM的一個(gè)核心的思想。對(duì)于MVC系列的其他模式变汪,由于界面設(shè)計(jì)和邏輯開發(fā)可以獨(dú)立的同時(shí)進(jìn)行侠坎,第一個(gè)好處是開發(fā)周期縮短,程序員再不需要等UI將設(shè)計(jì)圖拿給你才開始寫上層的功能代碼裙盾;第二個(gè)好處是界面設(shè)計(jì)師和程序員都可以在更專注的干好自己份內(nèi)的事实胸。
ViewModel層:ViewModel翻譯過來—視圖的模型,很恰當(dāng)番官。ViewModel就是完全反應(yīng)View的狀態(tài)和行為童芹,是View的內(nèi)在抽象。John Gossman 在他的博文中說什么鲤拿?他說ViewModel包含ViewState假褪、ValueConverter、Commands近顷、DataBindings生音,所以說ViewModel是一個(gè)抽象的View一點(diǎn)都沒錯(cuò)宁否。我在網(wǎng)上看到很多朋友錯(cuò)誤的理解,大家切記ViewModel不是數(shù)據(jù)模型的封裝缀遍,不是數(shù)據(jù)模型的封裝慕匠,不是數(shù)據(jù)模型的封裝,重說三域醇!從ViewModel的外在屬性來看台谊,ViewModel和Model層的數(shù)據(jù)模型半毛錢關(guān)系都沒有,它不過是使用了數(shù)據(jù)模型所攜帶的數(shù)據(jù)而已譬挚。另外锅铅,在MVVM模式的開發(fā)設(shè)計(jì)中,是重View和ViewModel减宣,而輕Model的盐须。當(dāng)然說輕Model,不是說你Model層就可以隨心所欲的設(shè)計(jì)漆腌,而是強(qiáng)調(diào)設(shè)計(jì)師的中心在界面上贼邓,程序員的重心在ViewModel上,最后將這精心設(shè)計(jì)的兩層binding起來闷尿,就可以保證咱們項(xiàng)目的高大上~塑径。好了,再解釋一下上面提到的幾個(gè)關(guān)鍵詞:
- ViewState 指數(shù)據(jù)的數(shù)據(jù)狀態(tài)和顯示狀態(tài)填具。數(shù)據(jù)狀態(tài)就是在視圖生命周期中展示的數(shù)據(jù)的值及其變化晓勇;顯示狀態(tài)就是視圖在生命周期中顯示成什么樣的。
- ValueConverter 用于格式化數(shù)據(jù)的灌旧,比如需求是將時(shí)間顯示為昨天今天明天,但是模型中是時(shí)間戳绰筛,我們就需要ValueConverter來對(duì)時(shí)間進(jìn)行格式化枢泰。
- Commands 包含了視圖的所有業(yè)務(wù)行為,比如登陸操作铝噩,就對(duì)應(yīng)一個(gè)登陸的command衡蚂,它將于登陸按鈕的點(diǎn)擊事件綁定起來,當(dāng)點(diǎn)擊事件發(fā)生骏庸,command內(nèi)封裝的登陸業(yè)務(wù)就會(huì)自動(dòng)觸發(fā)毛甲。
- DataBindings 不用解釋,肯定是指View和ViewModel的綁定了具被。一般的View中數(shù)據(jù)容器如textview玻募,跟ViewModel的ViewState中的數(shù)據(jù)字段綁定起來;View的展示屬性跟ViewModel的ViewState中的展示狀態(tài)綁定起來一姿;View的事件跟ViewModel中定義的命令綁定起來七咧。
Binder層:之所以要將Binder單獨(dú)拿出來說跃惫,是因?yàn)橐獙?shí)現(xiàn)一個(gè)穩(wěn)定的高效的,應(yīng)用廣泛的綁定機(jī)制實(shí)際是相當(dāng)復(fù)雜的艾栋,牽涉到很多問題爆存。正如微軟做的一樣,將Binder作為MVVM開發(fā)的基礎(chǔ)組件內(nèi)置在了平臺(tái)中蝗砾,讓開發(fā)者解放出來做更有意義的事情先较。
Controller層:MVVM模式雖然名稱里沒有“C”的字樣,但是并不代表沒有Controller悼粮,正式Controller將View和ViewModel關(guān)聯(lián)起來闲勺,當(dāng)然用的是Binder層提供的綁定機(jī)制。這里提一句:在iOS上矮锈,controller也可能負(fù)責(zé)界面的生命周期霉翔、View的組織等工作,但工作量顯然輕多了苞笨,這就是我們常常說的瘦身的作用债朵。
下面是一張簡(jiǎn)單的結(jié)構(gòu)圖
上圖中,View和ViewModel之間用的是虛線的箭頭相連瀑凝,這表明View和ViewModel沒有直接的引用關(guān)系序芦,他們各自對(duì)于另一方都是透明的。View和ViewModel是在Controller的控制下通過Binding機(jī)制綁定在一起粤咪,從而協(xié)同工作的谚中。
talk is cheap
接下來是一個(gè)MVVM的demo,實(shí)現(xiàn)的是跟上一篇MVP一樣的功能寥枝。由于代碼太多宪塔,這里只能展示一部分,完整demo大家可以進(jìn)到這里囊拜,歡迎star和fork某筐。
- 登陸界面View層
@interface LoginView : UIView
@property (weak, nonatomic) IBOutlet UITextField *accountField;
@property (weak, nonatomic) IBOutlet UITextField *pwdField;
@property (weak, nonatomic) IBOutlet UIButton *loginBtn;
@property (strong,nonatomic) VLoadingProperty * logging;
@property (strong,nonatomic) VAlertProperty * logErr;
@property (strong,nonatomic) VNavProperty * toMain;
@property (strong,nonatomic) VEditBehavior * editEnabled;
- (id)initWithFrame:(CGRect)frame controller:(UIViewController *)vc;
@end
@implementation LoginView
///MARK: 初始化
- (id)initWithFrame:(CGRect)frame controller:(UIViewController *)vc {
if ([self initWithFrame:frame]) {
self.vc = vc;
NSArray * arr=[[NSBundle mainBundle] loadNibNamed:@"LoginView" owner:self options:nil];
UIView * view = arr.firstObject;
if (view) {
[self addSubview:view];
view.frame=self.bounds;
//初始化屬性和行為
[self logging];
[self logErr];
[self toMain];
[self editEnabled];
}
}
return self;
}
///MARK: 屬性
- (VLoadingProperty *)logging {
if (!_logging) {
_logging= [VLoadingProperty new];
_logging.superView=self;
}
return _logging;
}
- (VAlertProperty *)logErr {
if (!_logErr) {
_logErr = [VAlertProperty new];
_logErr.vc = self.vc;
}
return _logErr;
}
- (VNavProperty *)toMain {
if (!_toMain) {
_toMain = [VNavProperty new];
_toMain.nav = self.vc.navigationController;
}
return _toMain;
}
///MARK: 行為
- (VEditBehavior *)editEnabled {
if (!_editEnabled) {
_editEnabled = [[VEditBehavior alloc] initWithView:self];
}
return _editEnabled;
}
@end
- 登陸界面ViewModel層
@interface LoginVM : NSObject<IViewModel>
@property(assign,nonatomic)BOOL logging;//正在登陸
@property(strong,nonatomic)MainViewController *main;//登陸成功后有效
@property(strong,nonatomic)NSString * logErr;//登陸失敗錯(cuò)誤
@property(strong,nonatomic)NSString * account;//輸入賬號(hào)
@property(strong,nonatomic)NSString * password;//輸入密碼
@property(strong,nonatomic)VMCommand * login;//登陸操作
- (void)start;
@end
大家會(huì)發(fā)現(xiàn)LoginView 中除了自身初始化和屬性初始化沒有任何的界面邏輯,而這一部分工作在WPF中則是用XAML來做冠跷;同時(shí)南誊,LoginViewModel中除了start方法也沒有直接定義任何的其他業(yè)務(wù)方法,跟LoginView中屬性幾乎是一一對(duì)應(yīng)蜜托。接下來我們看另外一個(gè)界面
3抄囚、我的朋友界面View層
@interface FriendListView : UITableView
- (id)initWithController:(UIViewController *)viewController;
@property(strong,nonatomic)VDataListProperty * datalist;
@property(strong,nonatomic)VAlertProperty * rmError;
@property(strong,nonatomic)VConfirmProperty * confirm;
@end
@implementation FriendListView
///MARK: 初始化
- (id)initWithController:(UIViewController *)viewController {
if (self=[self initWithFrame:CGRectZero style:UITableViewStylePlain]) {
_viewController=viewController;
}
return self;
}
///MARK: 屬性
- (VDataListProperty *)datalist {
if (!_datalist) {
_datalist = [VDataListProperty new];
_datalist.tableView = self;
_datalist.cellNib = @"FriendViewCell";
_datalist.cellHeight = 80;
_datalist.cellSelectionStyle = UITableViewCellSelectionStyleNone;
_datalist.cellEditStyle = UITableViewCellEditingStyleDelete;
_datalist.select = [VSelectBehavior new];
_datalist.edit = [VSelectBehavior new];
}
return _datalist;
}
- (VAlertProperty *)rmError {
if (!_rmError) {
_rmError = [VAlertProperty new];
_rmError.vc = self.viewController;
_rmError.title = @"刪除錯(cuò)誤";
}
return _rmError;
}
- (VConfirmProperty *)confirm {
if (!_confirm) {
_confirm = [VConfirmProperty new];
_confirm.vc = self.viewController;
_confirm.title = @"再次確認(rèn)";
}
return _confirm;
}
@end
4、我的朋友界面VM層
@interface FriendVM : NSObject<IViewModel>
@property(strong,nonatomic)UIImage * logo;
@property(strong,nonatomic)NSString * name;
@property(strong,nonatomic)NSString * signture;
- (id)initWithFriend:(Friend *)friend;
- (void)start;
@end
@interface FriendsVM : NSObject<IViewModel>
@property(strong,nonatomic)NSArray<FriendVM *> * friends;
@property(strong,nonatomic)VMCommand * rm;
@property(strong,nonatomic)NSString * confirm;
@property(strong,nonatomic)VMCommand * rm_hard;
@property(strong,nonatomic)NSString * rmError;
- (void)start;
@end
5橄务、Controller中綁定View與ViewModel并啟動(dòng)的代碼
登陸
dispatch_once(&onceToken, ^{
[[V2MBinder shared] registerMappings:@{
@"accountField.text":@"account",
@"pwdField.text":@"password",
@"loginBtn.touch":@"login",
@"logging":@"logging",
@"logErr":@"logErr",
@"toMain":@"main"
} betweenView:LoginView.class andVM:LoginVM.class];
});
[[V2MBinder shared]
bindView:self.loginView
withVM:self.vm];
[self.vm start];
我的朋友
[[V2MBinder shared] registerMappings:@{
@"headImgView.image":@"logo",
@"nameLabel.text":@"name",
@"signatureLabel.text":@"signture"
} betweenView:FriendViewCell.class andVM:FriendVM.class];
[[V2MBinder shared] registerMappings:@{
@"headView.image":@"logo",
@"nameLabel.text":@"name"
} betweenView:FriendViewItem.class andVM:FriendVM.class];
[[V2MBinder shared] registerMappings:@{
@"datalist":@"friends",
@"rmError":@"rmError",
@"confirm":@"confirm",
@"datalist.edit":@"rm",
@"confirm.sure":@"rm_hard"
} betweenView:FriendListView.class andVM:FriendsVM.class];
[[V2MBinder shared] registerMappings:@{
@"datalist":@"friends",
@"rmError":@"rmError",
@"confirm":@"confirm",
@"datalist.select":@"rm",
@"confirm.sure":@"rm_hard"
} betweenView:FriendGridView.class andVM:FriendsVM.class];
[[V2MBinder shared] bindView:self.listView withVM:self.vm];
[self.vm start];
另外說明一下幔托,demo中并沒有實(shí)現(xiàn)VM和Model之間的雙向綁定,由于這不是討論的重點(diǎn)蜂挪,也由于實(shí)現(xiàn)起來太多麻煩柑司,就沒有做這個(gè)功能迫肖,大家看demo的時(shí)候注意一下就行了。
總結(jié)
說了這么多攒驰,希望大家已經(jīng)對(duì)MVVM有一定認(rèn)識(shí)了蟆湖,如果還是是懂非懂,請(qǐng)一定要看一看這里的demo玻粪。結(jié)合代碼來看會(huì)更容易理解和記憶的隅津。下面再對(duì)MVVM模式做一下總結(jié):
- MVVM模式有利于界面設(shè)計(jì)和程序開發(fā)進(jìn)行更加明確的分工,提高產(chǎn)品質(zhì)量和開發(fā)效率劲室。
- MVVM模式由于業(yè)務(wù)邏輯和界面邏輯都完全脫離了UI伦仍,所以有利于進(jìn)行單元測(cè)試。
- MVVM模式中由于View層與ViewModel層完全解耦很洋,所以都具有很高的可復(fù)用性和擴(kuò)展性充蓝。
- MVVM模式中由于ViewModel被定義為View層的抽象,所以通過保存ViewModel喉磁,可以很容易的對(duì)View層進(jìn)行狀態(tài)恢復(fù)谓苟。
- MVVM模式中雙向綁定機(jī)制會(huì)對(duì)性能和代碼調(diào)試又一定的影響。
- MVVM模式實(shí)現(xiàn)起來比較復(fù)雜协怒,在沒有基礎(chǔ)開發(fā)平臺(tái)的支持的情況下涝焙,開發(fā)效率不容易提高,所以目前MVVM不適應(yīng)于iOS孕暇,andriod等開發(fā)平臺(tái)仑撞。相對(duì)來說,MVP模式更適用于上述兩個(gè)平臺(tái)妖滔。
關(guān)于MVVM模式就告一段落了隧哮,如果有什么疑問或發(fā)現(xiàn)什么錯(cuò)誤,歡迎在下方留言進(jìn)行討論和指正座舍,感謝大家的支持沮翔。
參考資料
https://blogs.msdn.microsoft.com/johngossman/2005/10/08/introduction-to-modelviewviewmodel-pattern-for-building-wpf-apps/
https://blogs.msdn.microsoft.com/johngossman/2005/10/09/100-modelviewviewmodels-of-mt-fuji/
https://blogs.msdn.microsoft.com/johngossman/2006/02/26/model-view-viewmodel-pattern-example/
https://blogs.msdn.microsoft.com/johngossman/2006/03/07/collectionview/#comment-1303
https://blogs.msdn.microsoft.com/johngossman/2006/03/04/advantages-and-disadvantages-of-m-v-vm/
https://blogs.msdn.microsoft.com/johngossman/2006/04/13/uml-diagram-of-model-view-viewmodel-pattern/