我所理解MVVM模式

前言

其实关于MVVM,笔者早就想谈谈自己的想法,跟朋友们交流学习。但是由于这段时间公司任务紧,加班多,而抽不出时间来。这样一来离上一篇MVP模式已经有两个月了。

起源

MVVM 最早于 2005 年被微软的 WPF 和 Silverlight 的架构师 John Gossman 提出,并不断完善。微软为了MVVM,可谓是费劲心力。为平台整合了大量基础设施和高级特性,如XAML、Blend、Bingding System、AttachBehavior、DependencyProperty等等,这些都大大的简化了MVVM的开发,使MVVM模式在微软平台得到了广泛应用。

MVVM在解决什么问题?

当面试官问你这个问题的时候,千万别只说MVVM是在给Controller瘦身,这样你一眼就被看穿对MVVM一知半解。任何一个模式的出现,一定为了解决软件工程中某个特定的痛点,要么是为了提高开发效率,缩短软件开发周期;要么是为了提高软件的稳定性、可扩展性;要么是为了提高软件的运行性能等等,“瘦身”不过是被捎带上的结果。那MVVM在解决什么问题呢?或者说MVVM为什么而出现呢?

MVVM是为了让界面设计师专注于界面元素和交互,从而能够设计出让用户欣喜若狂的产品;让开发者专注于逻辑,完全脱离UI,从而能够保证程序的稳定性、可扩展性、和高性能。由于界面设计和业务完全分离,使得更换界面变得简单,调整业务逻辑也不会对界面产生严重的影响;另一方面,也使得我们可以单独对业务进行单元测试。

模式解析

Model层:数据服务层,跟其他类MVC模式一样,不管最终对接的是数据库还是网络API或是其它,都是在负责数据的存储,并提供访问数据的接口,以支持数据的增删改查的基本操作。
View层:界面层,但大家注意,这里的View层相对于MVP模式中的View来说,指代的范围更狭小,原则上它不包含任何界面逻辑,在基本组件库满足需求的情况下,View层的设计和制作完全不需要程序员的参与,所有工作都由界面设计师完成,这也是MVVM的一个核心的思想。对于MVC系列的其他模式,由于界面设计和逻辑开发可以独立的同时进行,第一个好处是开发周期缩短,程序员再不需要等UI将设计图拿给你才开始写上层的功能代码;第二个好处是界面设计师和程序员都可以在更专注的干好自己份内的事。
ViewModel层:ViewModel翻译过来—视图的模型,很恰当。ViewModel就是完全反应View的状态和行为,是View的内在抽象。John Gossman 在他的博文中说什么?他说ViewModel包含ViewState、ValueConverter、Commands、DataBindings,所以说ViewModel是一个抽象的View一点都没错。我在网上看到很多朋友错误的理解,大家切记ViewModel不是数据模型的封装,不是数据模型的封装,不是数据模型的封装,重说三!从ViewModel的外在属性来看,ViewModel和Model层的数据模型半毛钱关系都没有,它不过是使用了数据模型所携带的数据而已。另外,在MVVM模式的开发设计中,是重View和ViewModel,而轻Model的。当然说轻Model,不是说你Model层就可以随心所欲的设计,而是强调设计师的中心在界面上,程序员的重心在ViewModel上,最后将这精心设计的两层binding起来,就可以保证咱们项目的高大上~。好了,再解释一下上面提到的几个关键词:

  • ViewState 指数据的数据状态和显示状态。数据状态就是在视图生命周期中展示的数据的值及其变化;显示状态就是视图在生命周期中显示成什么样的。
  • ValueConverter 用于格式化数据的,比如需求是将时间显示为昨天今天明天,但是模型中是时间戳,我们就需要ValueConverter来对时间进行格式化。
  • Commands 包含了视图的所有业务行为,比如登陆操作,就对应一个登陆的command,它将于登陆按钮的点击事件绑定起来,当点击事件发生,command内封装的登陆业务就会自动触发。
  • DataBindings 不用解释,肯定是指View和ViewModel的绑定了。一般的View中数据容器如textview,跟ViewModel的ViewState中的数据字段绑定起来;View的展示属性跟ViewModel的ViewState中的展示状态绑定起来;View的事件跟ViewModel中定义的命令绑定起来。

Binder层:之所以要将Binder单独拿出来说,是因为要实现一个稳定的高效的,应用广泛的绑定机制实际是相当复杂的,牵涉到很多问题。正如微软做的一样,将Binder作为MVVM开发的基础组件内置在了平台中,让开发者解放出来做更有意义的事情。
Controller层:MVVM模式虽然名称里没有“C”的字样,但是并不代表没有Controller,正式Controller将View和ViewModel关联起来,当然用的是Binder层提供的绑定机制。这里提一句:在iOS上,controller也可能负责界面的生命周期、View的组织等工作,但工作量显然轻多了,这就是我们常常说的瘦身的作用。

下面是一张简单的结构图


MVVM模式结构

上图中,View和ViewModel之间用的是虚线的箭头相连,这表明View和ViewModel没有直接的引用关系,他们各自对于另一方都是透明的。View和ViewModel是在Controller的控制下通过Binding机制绑定在一起,从而协同工作的。

talk is cheap

接下来是一个MVVM的demo,实现的是跟上一篇MVP一样的功能。由于代码太多,这里只能展示一部分,完整demo大家可以进到这里,欢迎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;//登陆失败错误

@property(strong,nonatomic)NSString * account;//输入账号

@property(strong,nonatomic)NSString * password;//输入密码

@property(strong,nonatomic)VMCommand * login;//登陆操作

- (void)start;

@end

大家会发现LoginView 中除了自身初始化和属性初始化没有任何的界面逻辑,而这一部分工作在WPF中则是用XAML来做;同时,LoginViewModel中除了start方法也没有直接定义任何的其他业务方法,跟LoginView中属性几乎是一一对应。接下来我们看另外一个界面
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 = @"删除错误";
    }
    return _rmError;
}

- (VConfirmProperty *)confirm {
    if (!_confirm) {
        _confirm = [VConfirmProperty new];
        _confirm.vc = self.viewController;
        _confirm.title = @"再次确认";
    }
    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并启动的代码
登陆

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中并没有实现VM和Model之间的双向绑定,由于这不是讨论的重点,也由于实现起来太多麻烦,就没有做这个功能,大家看demo的时候注意一下就行了。

总结

说了这么多,希望大家已经对MVVM有一定认识了,如果还是是懂非懂,请一定要看一看这里的demo。结合代码来看会更容易理解和记忆的。下面再对MVVM模式做一下总结:

  1. MVVM模式有利于界面设计和程序开发进行更加明确的分工,提高产品质量和开发效率。
  2. MVVM模式由于业务逻辑和界面逻辑都完全脱离了UI,所以有利于进行单元测试。
  3. MVVM模式中由于View层与ViewModel层完全解耦,所以都具有很高的可复用性和扩展性。
  4. MVVM模式中由于ViewModel被定义为View层的抽象,所以通过保存ViewModel,可以很容易的对View层进行状态恢复。
  5. MVVM模式中双向绑定机制会对性能和代码调试又一定的影响。
  6. MVVM模式实现起来比较复杂,在没有基础开发平台的支持的情况下,开发效率不容易提高,所以目前MVVM不适应于iOS,andriod等开发平台。相对来说,MVP模式更适用于上述两个平台。

关于MVVM模式就告一段落了,如果有什么疑问或发现什么错误,欢迎在下方留言进行讨论和指正,感谢大家的支持。

参考资料

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/

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345

推荐阅读更多精彩内容