iOS开发为什么我喜欢用MCC模式而不用MVC/MVP/MVVM

iampat· 2019-11-18

在讲我用的 MCC 模式前 ,我默认大家熟知 MVC/MVP/MVVM 这三种模式。

为了简化表达,在这篇文章里 VC 代表 ViewController,C 代表 Controller,标准模式 指的是 MVC/MVP/MVVM 这三种模式,MCC 的全称是 Model ViewController Controller

摘要

最初开发时我们学的都是官方的 MVC 模式,实际使用中,我刚开始做项目时往往会省略 M 这部分,因为有些模型非常简单,项目又赶,我连新建一个模型类的时间也不想投入。而对于 V 这部分,往往会嵌套很多子 View 来拼装,有时甚至会把一些按钮时间和跳转写到 V 里面,这使得后期寻找入口更困难。对于控制器存在的问题往往是它非常臃肿,我什么都想往里放,久而久之就成了一个垃圾桶。这些是我实际开发遇到的问题,开发中因为赶进度、懒、写一个类试下能不能实现等问题导致我不严格遵照某种开发模式,这并不是故意为之,而是开发习惯使我趋向于这样。就犹如熵趋向于变大,桌面趋向于混乱一样。而使我产生这种情况的原因归纳起来是因为 MVC 这个模式 ‘不够好用’。因为模式的不足之处使我不习惯、或者说不喜欢遵从它的规则。简单的说,如果我用的很爽,我肯定就会下意识的照着去做了。接下去我会介绍我比较喜欢的 MCC 模式,我并不是说我要提到的 MCC 比哪种模式要好,而是对我而言,在实际使用来中 MCC 使我用的更爽,所以我更喜欢用它。

MCC 结构

前面说 MCC 的全称是 Model ViewController Controller。那么 View 跑哪里去了呢,它们就在最后一个 C 中,就是 Controller 中。Controller 的角色是控制,它是驱动和总控,所以它肯定对其他要调用的类有依赖(除非用 oc 的运行时发送消息,这会使开发变得复杂,非常不好用)。那么一个 Controller 就是一个小生态,就如一个小家庭一样,除了有具体的任务外,要管理各种琐事。比如一个家庭可能要烧饭,那么就会随之而来倒垃圾这个琐事产生。你会发现开发中很多函数是随意的、灵活的、不可预判的,你在做需求前都没有想过会写这个函数,但是实际开发中为了达成某个目的这个函数就产生了,它是那么重要甚至你当下都找不到更好的方法替换掉它。这时 Controller 发挥很大作用,你可以大胆放入,因为它就是处理琐事的集合,这也节省了你的思考,到底要把函数放在哪里。那么 C 就是一个app内的最小单元,它是一个完整功能单元,可以独立运行。而不像标准模式那样需要结合才是一个整体,单独的类只有功能的一部分。所以这里的 C 也可以理解成 Component 的意思。所以 C 中不仅有视图 view 还有视图间的关系代码和交互代码,但是如果涉及到模块外的交互,C 通过代理的方式回调到 VC 中处理。这样的好处是解除模块的依赖,当你把这个 C 给其他 VC 使用,或者拷贝 C 到另一个app使用时,不需要处理依赖问题,因为 C 之间的依赖代码都在 VC 之中。
我们看一下上面提到的两种封装方法,假设要封装一个头像用户名的常用模块:

用 MCC 的模式 M 的作用还是模型,VC 的作用是处理 C 之间的关系以及处理 VC 直接的关系。如果把 app 比作我们国家的话,VC 就是省,C 就是省下面的市。区别是这个市可以被另一个省快速调用,就是不同省可以快速通往同一个市。按这种结构区分,通过看 VC 快速梳理一个界面中的模块关系,再找对应的模块修改,可以一次只关注一个问题,对于团队开发也可以减少团队成员的认知负荷。认知负荷是我在软件设计的哲学 (译 A Philosophy of Software Design)这篇文章中提到的,减少学习成本,就是用多久去学会使用一个东西的概括。

职责分布

这个模式下弱化了 View 的存在,实际上是放在 C 之中的,但我们认为 View 不是单独存在的,View 的出现必然包含着 View 直接的关系代码、交互代码以及逻辑代码,所以他是在 C 中混合存在的。我在开发初期经常会封装一些 View,如 stateView、orderView等。实际上他们都是一些 view 和 label 的组合体,这些 View 不仅仅承担了 View 的功能,还有 View 之间的关系代码,甚至一些点击交互代码。那么这个混合 View 并不是严格意义上的 View 了,它们是有控制代码的 View,那么用 C 去描述似乎更合理,因为只有控制器才适合放视图之间的关系代码。
我们看一下一个app中的职责分布:

结合实际开发情况,严格遵守标准模式会浪费部分开发时间,特别是对于一个很简单的控制器来说。所以在 MCC 的模式下,ViewController 是可以充当 Controller 的角色的,C 的出现是为了解决 VC 的臃肿问题,所以它们之间不仅是包含关系,还是互补关系。所以对于一个简单的控制器。我们只需把 VC 当做 C 来使用。

再看对于一个复杂的控制器。一个复杂的控制器一定可以拆分成小块,可以按功能拆分,也可以按结构拆分。将功能分散到各个 C 中单独处理,这样可以一次只关注一个问题。

开发时的逻辑

开发时的逻辑就是,根据页面建一个 VC,然后看这个 VC 内是不是可以拆分模块,如果很简单就把 VC 当 C 用,如果拆分的 C 很复杂,看能不能把这个 C 拆成两个平级的 C,在 VC 中调用。不要出现嵌套,就是 C 中不再嵌套 C,所有的 C 在 VC 中平铺开来。你在 VC 中可以马上看到 C 之间的调用关系。同时每个 C 都能快速在另一个 VC 中使用而不用改这个 C 中的代码。
我们看一下设计开发时的思考过程:

对于 CC_Controller 和 CC_ViewController 的关系的关系,我在bench_ios框架的设计思考,基础库、runtime和组件化 这篇文章中已经提过。哪个大,CC_ViewController 大。为什么取 CC_ViewController 去包含 CC_Controller,有几个原因:
1、整个app是从 UIKit 中的 UIApplicationMain() 方法启动的,可见 UI 是app应用层的支柱,apple没有做 Controller 这个类,只有 ViewController。 2、如果反过来和iOS默认开发模式有冲突,必须比之前多建一个 Controller 去管理 ViewController,操作上也不方便。 3、使用多个 Controller 更好拆分,分发给别的模块。

任何架构的设计目的都是为了:低耦合,高复用,易测试,好维护。 我们逐个分析以上几点在 MCC 模式下的表现。

低耦合:

因为我们把模块间的依赖代码都写到 VC 中,C 之间是没有依赖的。又因为我们在 VC 中以注册 C 的方式使用,那么只有 VC 对 C 的依赖,C 对 VC 也是没有依赖的。所以 作为最小单元的 C 是可以独立存在的,说直白点,就是把一个 C 拷贝出来编译是可以通过的。因为依赖小,所以耦合低。

高复用:

前面说了我们开发时用效果图的控制器标题来创建 VC ,实际上 VC 是起到一个壳的作用,核心是在 C,这些 C 是可以高度复用的。因为注册的是实例,不同的 VC 可以注册同一个 C。这就实现了高复用。

易测试:

我们来讲如何找bug,因为有职责区分,可以先判断这个bug是模块间交互问题还是单独模块内问题,如果是前者,去相应的 VC 找问题,后者就去对应的 C 找问题。那么反过来也可以走通,如果不能判断这个bug是什么问题导致,可以通过看 VC 整理思路,理清模块间的交互,再深入 C 去排查问题。

好维护:

维护可以分为修改和新增(删减)。这条和易测试相似,修改也是照着测试找bug的思路分析。对于新增(删减),也只是添加一个 C ,然后在要使用的 VC 中注册。如果新增的 C 需要和其他模块交互,在使用的 VC 中写沟通代码。

我对这个模式提供的功能封装:

这个模式的运用是基于bench_ios这个框架的基础上的。通过扩展 VC 的生命周期来快速使用 MCC 模式,下面我用一些实际模块中代码来举例如何运用。

  1. 创建一个 CC_Controller 的子类。
  2. 在 cc_willInit 函数中初始化代码。(默认注册后就会调用的函数)
  3. 在 VC 中注册。(注册一个充值输入模块和一个充值方式选择模块)
- (void)cc_viewWillLoad {

    self.cc_title = @"充值";

    _chargeMoney = [ccs init:ChargeMoneyInputController.class];
    [self cc_registerController:_chargeMoney];

    _method = [ccs init:ChargeMethodController.class];
    [self cc_registerController:_method];
}
  1. 交互,添加代理。
    例如我对充值输入金额封装一个 C,那么在修改金额后,修改的金额或者需要的手续费要传出去给支付模块。那么先在 h 文件声明协议:
@protocol ChargeMoneyInputControllerDelegate <NSObject>

- (void)controller:(ChargeMoneyInputController *)controller moneyChanged:(CC_Money *)money;

@end

在 m 文件用 cc_performSelector: 发送事件和参数:

- (void)updateText {

    CC_Money *money = [ccs init:CC_Money.class];
    [money moneyWithString:_moneyTextField.text];

    CCLOG(@"%@", money.value);
    [self.cc_delegate cc_performSelector:@selector(controller:moneyChanged:) params:self, money];
}

cc_performSelector 默认会判断是否实现了协议方法,即包含了 if (delegate && [delegate respondsToSelector:selector]) 的代码。

  1. 在 VC 中和其他 C 交互:
- (void)controller:(ChargeMoneyInputController *)controller moneyChanged:(CC_Money *)money {

    [_method updatePay:money];
}

因为更新了金额,通知另一个 C 做出改变。

总结

通过我自身的实践来看,对于中小型app的开发来说,我更喜欢用 MCC 模式。这也比较适合中小型创业公司,因为对于创业公司来说,项目快速变动是常事,团队需要快速开发一个应用去试错,所以应用的特点就是不确定。因此我们需要一个高机动性的结构去适应变化。通过 MCC 模块的拆分,我们把一部分需求通过视图代码、逻辑代码封装到一个 C 中,这个 C 就是独立需求,它与其他模块的交互全部以代理方式代理出去,交给使用方配置。这会使得我们每多做一个项目,都能保留一些 C ,以备后期产品又需要类似需求能快速集成。所以 MCC 模式并没有像 MVC 那样严格划分责任,C 看起来更像一个大杂烩,但它却是一个拥有独立运行能力的最小单元。略有区别于目前流行的积木式组件,单个积木并没有独立运行的能力,它不是一个完整系统,所以我觉得称之为变形金刚为准确,它们可以各自为战亦可互相组合变得更强。这也符合 bench_ios框架的设计思考,基础库、runtime和组件化 的设计思路。也希望有大量应用去验证这个模式,欢迎大家去留言交流。