我所理解MVVM模式

前言

其實(shí)關(guān)于MVVM,筆者早就想談?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等等,這些都大大的簡化了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ù)完全分離,使得更換界面變得簡單,調(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é)構(gòu)圖


MVVM模式結(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。

  1. 登陸界面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
  1. 登陸界面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é):

  1. MVVM模式有利于界面設(shè)計(jì)和程序開發(fā)進(jìn)行更加明確的分工,提高產(chǎn)品質(zhì)量和開發(fā)效率。
  2. MVVM模式由于業(yè)務(wù)邏輯和界面邏輯都完全脫離了UI,所以有利于進(jìn)行單元測(cè)試。
  3. MVVM模式中由于View層與ViewModel層完全解耦,所以都具有很高的可復(fù)用性和擴(kuò)展性。
  4. MVVM模式中由于ViewModel被定義為View層的抽象,所以通過保存ViewModel,可以很容易的對(duì)View層進(jìn)行狀態(tài)恢復(fù)。
  5. MVVM模式中雙向綁定機(jī)制會(huì)對(duì)性能和代碼調(diào)試又一定的影響。
  6. 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/

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

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

  • 談?wù)?MVX 中的 Model 談?wù)?MVX 中的 View 談?wù)?MVX 中的 Controller 淺談 MV...
    Draveness閱讀 15,160評(píng)論 0 59
  • 前言: 本文主要是對(duì)常見設(shè)計(jì)模式的一些分析,以及講述在Android項(xiàng)目中實(shí)現(xiàn)Mvvm模式的兩種方式。通過Data...
    Yagami3zZ閱讀 45,493評(píng)論 29 117
  • 前言 談起MVC,MVP和MVVM這三個(gè)最耳熟能詳?shù)腁ndroid框架,相信大家對(duì)它們都不陌生,但在實(shí)際的情況下,...
    ghroost閱讀 4,024評(píng)論 0 40
  • 姓名:徐萍 常州新日催化劑有限公司 六項(xiàng)精進(jìn)422班:【反省二組】 【日精進(jìn)打卡第14天】 【知~學(xué)習(xí)】 讀書半小...
    蘋果_4735閱讀 127評(píng)論 0 0
  • 下班和團(tuán)隊(duì)談了個(gè)業(yè)務(wù)。然后開始做開業(yè)前準(zhǔn)備,有點(diǎn)小激動(dòng)呢!家人們也都來體驗(yàn)館幫忙,感動(dòng)! 舔臉:這是汪汪隊(duì)中毒太深...
    Joyce_kexin閱讀 184評(píng)論 0 1

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