iOS 实用技术iOS开发实用工具

论MVVM伪框架结构和MVC中M的实现机制

2017-07-01  本文已影响6277人  欧阳大哥2013

目录

一直都有人撰文吹捧MVVM应用开发框架,文章把MVVM说的天花乱坠并且批评包括iOS和android所用的MVC经典框架。这篇文章就是想给那些捧臭脚的人们泼泼冷水,虽然有可能招致骂声一片,但是目的是给那些刚入门的小伙伴一些参考和建议,以免误入歧途。同时也给那些深陷其中不能自拔的小伙伴们敲敲警钟,以免其在错误的道路上越走越远。

------ MVVM并非框架,而只是简单的文件夹分类 ------

MVVM被引入的前因后果

大概是在2010年左右移动端开发火了起来,起初是iOS,Android, WinPhone三个大平台竞争,后来后者退出了角逐,变成了二分天下。从应用体系结构以及为开发者提供的框架体系来看,两个平台都是推出了经典MVC三层结构的开发方式,这三层所代表的意义是模型、视图、控制。这个开发框架的初衷其实也很简单:视图负责展示和渲染,模型负责业务逻辑的实现,控制负责调度视图的事件以及业务逻辑的调用以及通知视图的刷新通知。 三部分松散耦合,各司其职。下面是经典的MVC框架结构:

MVC框架图

一个很可惜的事实是不管是Android和iOS都只对C和V两部分进行了标准的定义和实现:Android的视图部分的实现是定义了各种控件以及通过XML文件来组装视图布局界面,iOS的视图的实现也是定义了各种控件以及通过XIB或SB来组装视图布局界面; Android的控制部分则是通过Activity来实现,而iOS的控制部分则是通过UIViewController来实现的。而模型部分呢?因为每个应用的业务逻辑和应用场景并不相同,所以两个平台也无法也不能够定义出一个通用的模型层出来,而是把模型层的定义留给了开发者来实现。然而这为我们的开发者在使用MVC框架开发应用时埋下了隐患。

早期的应用开发相对简单,因为没有标准的模型层的定义,而控制层又在工程生成时留下了很多可供开发者写代码的地方,所以很多开发人员就自然而然的将业务逻辑、网络请求、数据库操作、报文拼装和解析等等全部代码都放入了控制层里面去了,根本就不需要什么模型层的定义。 这样随着时间的推移和应用的复杂增加,就出现了C层膨胀的情况了。一个控制器的代码可能出现了好几千行的场景。于是乎有人就开始找解决方案来为C层瘦身了。又一个很可惜的事实是还没有人去想着抽象出M层,而是用了如下方法来解决问题:

MVVM各层的依赖关系

真的是这样吗?答案是NO!!!

首先我想说的是一个优秀的框架中各层次的拆分并不是简单的将代码进行归类和划分,层次的划分是横向的,而模块的划分则是纵向的 。 这其中涉及到了层次之间的耦合性和职责的划分,以及层与层之间的交互接口定义和方式,同时层内的设计也应该具有高度的内聚性和结构性。而这些设计的要求并没有在所谓的MVVM中体现出来。

MVVM据说是来源于微软的数据视图的双向绑定技术。也就是有一个VM的类来实现数据的变化更新视图,视图的变化更新数据的处理,整个过程不需要再单独编码去处理。这个技术就和早年MFC里面的DDX/DDV技术相似。MVVM只是一种数据绑定技术的变种而不足以称为框架。框架中的层的要素要具有职责和功能的属性。就MVVM中所定义的M只能理解为纯数据。纵观整个iOS和android中的所有系统框架库都没有出现过让一批数据结构组成一个层的概念。即使如所谓的存储层也是数据库和表以及数据库引擎三者的结合体为一层。 其实之所以说控制器膨胀根源在于我们的手写布局视图在控制器中完成这里占用了非常多的代码, 业务处理和实现也在控制器中完成。苹果和Google已经给出了通过SB和XML来实现视图的构建。至于复杂的业务逻辑也完全可以通过拆分为多个子视图控制器或者多个Fragment 来完成。请问如果一个设计的足够好的C层,何来膨胀这么一说!

------ MVVM被引入的根本原因是对M层的错误认识所引起的 ------

MVC中M层实现的准则

说了那么多,可以总结出所谓的MVVM其实并不是一种所谓的框架或者模式,他只是一个伪框架而已,他只是将功能和处理按文件夹的方式进行了划分,最终的的结果是系统乱成了一锅粥。毫无层次可言,所具有的唯一优点是把C层的代码和功能完全弱化了。其实出现这种设计方法最根本的原因就是没有对M层进行正确的理解定义和拆分。那么我们应该如何正确的来定义和设计M层呢?下面是我个人认为的几个准则(也许跟其他人的理念有出入):

三层之间的单向依赖关系

只有当你系统设计的不同部分都是单向依赖时,才可能方便的进行层次拆分以及每个层的功能独立替换。

M层内部的封装层次 两种不同的M层封装实现

我们还可以进一步的对业务逻辑抽象出M层的接口和实现两部分,这样的一个好处是相同的接口可以有不同的实现方式,以及M层可以隐藏非常多的内部数据和方法而不暴露给调用者知道。通过接口和实现分离我们还可以在不改变原来实现的基础上,重新重构业务部分的实现,同时这种模式也很容易MOCK一个测试实现,这样在进行调试时可以很简单的在真实实现和MOCK实现之间切换,而不必每次都和服务器端进行交互调试,从而实现客户端和服务器之间的分别开发和调试。下面是一个升级版本的M层体系结构:

基于接口的M层实现
typedef void (^UICallback)(id obj, NSError * error);

这种模式其实在很多系统中有应用到。大家可以参数考苹果的CoreLocation.framework中的地理位置反解析的类CLGeocoder的定义。还有一点的是在AFN以及ASI中的网络请求部分都是把成功和失败的处理分成了2个block回调,但是这里建议在给C层的异步通知回调里面不区分2个block来调用,而是一个block用2个参数来解决。因为有可能我们的处理中不管成功还是失败都可能有部分代码是相似的,如果分开则会出现重复代码的问题。

MVC中M层实现的简单举例

最后我们以一个简单的用户体系的登录系统来实现一个M层。

1.定义标准的M层异步回调接口:

//定义标准的C层回调block。这里面的obj会根据不同对象的方法的返回而有差异。
typedef void (^UICallback)(id obj, NSError * error);

//这里定义标准的数据解析block,这个block供M层内部解析用,不对外暴露
typedef id (^DataParse)(id retData, NSError * error);

2.定义所有M层业务类的基类,这样在通用基类里面我们可以做很多处理。比如网络层的统一调用,加解密,压缩解压缩,我们还可以做AOP和HOOK方面的处理。

     @interface  ModelBase
          
           //定义一个停止请求的方法
           -(void) stopRequest;
           /**
             *定义一个网络请求的唯一入口方法
             * url 请求的URL
             * inParam: 入参
             * outParse: 返回数据解析block,由派生类实现
             * callback: C层通知block
             */
           -(void) startRequest:(NSString*)url  inParam:(id)inParam outParse:(DataParse)outParse  callback:(UICallback)callback;
     @end

3.定义一个用户类:

    @interface  ModelUser:ModelBase
  
        @property(readonly) BOOL isLogin;
        @property(readonly) NSString *name;
       
       //定义登录方法,注意这个登录方法的实现内部可能会连续做N个网络请求,但是我们要求都在login方法内部处理,而不暴露给C层。
       -(void)login:(NSString*)name  password:(NSString*)password   callback:(UICallback)callback;
        //定义退出登录方法
       -(void)logout:(UICallback)callback;
    @end

  

4.定义一个M层总体系统类(可选),这个类可以是单例对象:

    @interface ModelSystem:ModelBase
 
     +(ModelSystem*)sharedInstance;

    //聚合用户对象,注意这里是readonly的,也就是C层是不能直接修改用户对象,这样保证了安全,也表明了C层对用户对象的使用权限。
    @property(readonly)  ModelUser *user;  

    //定义其他聚合的模块

    @end

5.在C层调用用户登录:

  @implementation LoginViewController

    -(IBAction)handleLogin:(UIButton*)sender
   {
        sender.userInteractionEnabled = NO;
        __weak LoginViewController  *weakSelf = self;
       [[ModelSystem sharedInstance].user  login:@"aaa" password:@"bbb"  callback:^(ModelUser *user, NSError *error){

        if (weakSelf == nil)
               return;
       sender.userInteractionEnabled = YES;
       if (error == nil)
       {
              //登录成功,页面跳转
       }
       else
      {
            //显示error的错误信息。。
      }}];
         
   }

   @end

可以看出上面的C层的部分非常简单明了,代码也易读和容易理解。同时我们还看到了C层跟本不需要知道M层的登录实现到底是如何请求网络的,以及请求了几个网络操作,以及用的什么协议,以及什么数据报文格式,所有的这一切都封装在了M层内部实现了。C层所要做的就是简单的调用M层所提供的方法,然后在callback中通知界面更新即可。整个C层的逻辑也就是几十行就能搞定了。

具体的模型层设计方法请参考M层的设计


欢迎大家关注我的github地址,关注欧阳大哥2013,关注我的简书地址:http://www.jianshu.com/u/3c9287519f58

上一篇下一篇

猜你喜欢

热点阅读