iOS架构补完计划--浅谈MVC及其衍生架构模式(附简易图解)

目录

  • 概述
  • 傻瓜图解

  • MVC
    • 一个正统的MVC、三者的任务是什么?
    • 关于View到底该不该写一些业务代码
  • 胖Model与瘦Model
    • 强业务、弱业务
    • 胖Model
    • 瘦Model
    • 该用哪个?
  • MVVM
    • Model
    • View
    • ViewModel
    • Controller
    • ReactiveCocoa对于MVVM的意义是什么?
  • MVCS
  • MVP
  • VIPER
  • 关于架构设计、一些观点
    • 控制好Controller的代码量
    • 对于MVX如何选择
    • 无论用哪种模式、都要深刻的理解每个模块不同的职责

概述

其实只要是架构上的设计、本质上都是三个角色:数据管理者数据加工者数据展示者
不管是MVC、MVVM、MVP、VIPER或者任何新的设计模式、都跳不出这三个角色。无非是把数据管理者的工作进行拆分、唯一的界定标准就是把工作拆分的粒度大小。
而无论哪种思想、最终都逃不开三个问题的取舍。代码量通用性可读性

这里我主要写的是MVC和MVVM、对于其他的架构只是略微提及。

傻瓜图解

这两天总有人跟我说我想看Demo...我想了想觉得Demo其实也不太直观、干脆画几个图好了。
主要是想表达每个模块里应该放什么类型的东西、线可能有连得不对的地儿。亲们尽量意会吧。

  • MVC

  • MVC--胖Model

  • MVC--瘦Model

  • MVC--胖瘦结合

  • MVVM

  • MVP

  • VIPER

试了试...感觉画着费劲。能理解了前面几个的话、看文字应该也能理解VIPER吧。


MVC

MVC就是典型的着重通用型与可读性、这正是一个作为万物之初的架构所需要保证的事。简单、易学。

  • Model进行数据管理
  • View进行数据展示
  • Controller负责根据需求对Model以及View进行调配。

不过和广义的MVC不同、客户端(别的我不知道啊、起码iOS)由于UIViewController自带一个容器View、所以除了上述的正统任务之外、Controller还需要承担View的生成、布局等的任务。

一个正统的MVC、三者的任务是什么?

所以、我们可以将MVC三者的任务再进一步细化一下

  • Model:
    • Controller的读取提供数据
    • Controller的写入提供接口
    • Controller提供基本的业务组件

最常用的就是网络请求之后Json转Model、写入数据库之前Model转Json。

  • View:
    • 界面的展示
    • 响应与业务无关的事件(动画效果、点击反馈、点击事件的开关保护等等)

何为业务事件:Model数据的改变、网络请求的发送、页面的跳转、页面的刷新等等。

  • Controller
    • 管理self.view的生命周期
    • 负责生成所有的View实例、以及布局。
    • 将恰当的Model交付给View展示。
    • 监听来自View与业务有关的事件、通过与Model的合作、来完成对应事件的业务。
  • 关于View到底该不该写一些业务代码

其实自己以前。也会图方便、把一些自认为的弱业务写在View里。

举个例子:
  • 一个Cell、有用户Nickname、还有一个用户头像的Button。

点击事件肯定由Cell捕获。这个时候跳转用户主页的动作、该由View完成、还是传递给Controller
假设我们交由View、也就是当前的Cell跳转了。很方便、省去了写代理的小十行代码。
并且这个Cell也可以挪到其他页面去使用、一样能跳到用户主页、又省去不少代码。

  • 有一天、产品让你在某个页面点击头像不执行任何动作。

咋办呢?机智如你、给这个Cell添加了一个bool值来控制是否跳转就好了。

  • 又过了几天、产品让你在某个页面把这个头像弄成点击之后弹出举报框。

这怎么搞呢?也不是不行、你又给这个Cell添了一个枚举的type。这简直完美、不同的Type执行不同的事件、你顺便取消了那个bool、把他也写成了一个type

  • 又过了一阵子、产品又告诉你当满足某些条件的时候、这个头像跳主页。另一些条件的时候、这个头像不能跳。

于是、你终于写了个block或者代理、在点击之后执行一下再看下一步怎么跳转。
此时、回过头来再看你的Cell、已经面目全非了、充斥着各种业务判断。

可能你会说、如果产品真的这么二逼。那我干脆再copy一个Cell就好了啊。

但是别忘了、你当初这么设计这个Cell的时候可是为了节省下页面跳转的小十行代码、而你现在却要为此付出copy一整个Cell的代价。

其实还有一个更重要的问题、就是你这个View的模块复用基本为0

假设你需要另起一个新的工程写一个demo、如果用这个View你首先要解决一大堆跳转代码上Controller 文件的缺失、然后还会发现、原来写的很多逻辑、type在这个demo里毫无用处。挨个删除、梳理逻辑又要耗费很多时间。

而这些将来会发生的问题、如果你最开始不把业务事件代码硬写进View里、一件都不会发生。

胖Model与瘦Model

这里先要引出两个概念。强业务弱业务
二者关键的区别是代码变动的频率大小与涉及模块的多少。举两个例子:
1、比如把时间戳转化小数点的格式化或者修改A属性进行一系列计算并且改变B属性、这种业务就属于弱业务。
2、再比如一个一个订单Model的确认收货、就应该归入没办法归入弱业务、因为涉及网络请求、加密等等多个底层模块。

  • 胖Model

主旨是Controller从Model里拿到的数据、不需要进行更多的判断、处理等操作、就能使用。举个例子:

Raw Data:
    timestamp:1234567

FatModel:
    @property (nonatomic, assign) CGFloat timestamp;
    - (NSString *)ymdDateString; // 2015-04-20 15:16
    - (NSString *)gapString; // 3分钟前、1小时前、一天前、2015-3-13 12:34

Controller:
    self.dateLabel.text = [FatModel ymdDateString];
    self.gapLabel.text = [FatModel gapString];

这就需要将弱业务、写进Model、很好的满足的复用的需求。
胖Model也是存在问题的、就是移植的困难。毕竟业务再弱、也是代码、当项目成长到一定程度、这个Model也将会变得相当的臃肿。

  • 瘦Model

就是要把MVC的M贯彻倒底、除了业务的表达啥都不管。
但是这样又会导致Controller中的代码变得异常臃肿(废话么、连时间戳转化都要交给Controller不肿才怪)
所以瘦Model要借助一些外来的辅助模块(索性可以叫Helper)来对弱业务做抽象。举个例子:

Raw Data:
{
    "name":"casa",
    "sex":"male",
}

SlimModel:
    @property (nonatomic, strong) NSString *name;
    @property (nonatomic, strong) NSString *sex;

Helper:
    #define Male 1;
    #define Female 0;
    + (BOOL)sexWithString:(NSString *)sex;

Controller:
    if ([Helper sexWithString:SlimModel.sex] == Male) {
        ...
    }
  • 该用哪个?

我个人用胖Model用的比较多、但是也借助了一些瘦Model的思想。举例来讲:
除了上述很明确的可以放到Model里的弱业务之外、像一个订单中的确认收货、发货、申请退货等等操作、他们既不算特别强的业务、而且还有很高的复用需求(订单列表和订单详情都需要确认收货)。

这种业务有一种特点、就是代码就在哪里。不管你放到哪、都只能挪不能删。但挪到哪、都不完全合适。从定义上来讲十分莫若两可。个人觉得:
  • 这种的业务:能不放在Controller里就不要放
    • 你可以干脆放到胖Model里、毕竟将来拆分一个400行的Model、比拆分一个1400行的Controller容易得多。
    • 你也可以单独新建一个Helper、配合着Model来完成业务。这样想移植页面就单用Model、想带业务移植就带着Helper(其实这个思路已经很接近MVP了、但是还差提点。MVP还需要为View提供数据)。

MVVM

弱弱的一说、我并不推荐iOS中写MVVM(因为入侵性实在太强)、不会教你怎么用RAC怎么写出MVVM、只是想让你理解什么是MVVM
MVVM现在已经是一种非常成熟的思想了。应用也十分普及、例如Vue以及小程序。
MVVM的初衷也是为了Controller减负。
刚才的胖Model只从Controller移植走了一些简单的弱业务。
ViewModel则干脆把数据的处理全部从Controller移植了出去。
理想上相同的输入(比如网络服务响应)将会导出相同的输出(属性的值)。

简单的说一下M、V、VM的在架构中所扮演的角色。

  • Model:
和正统MVC中的瘦Model一样、只承载最基本的数据单元。
@interface UserListModel: NSObject
  
@property (nonatomic, strong, readonly) NSString *userName;
@property (nonatomic, strong, readonly) UIImage *portraitImg;

@end
  • View
其实也和正统的MVC一样、只做展示工作、不承接任何业务逻辑。

但是需要注意的是、有时候也会在View中将ViewModelView做一些绑定工作(ViewModel本质上也算是Model层、所以View并不适合直接持有ViewModel)。

- (void) awakeFromNib {
    [super awakeFromNib];
    RAC(self. portraitImgView,  image) = RACObserve(self,  viewModel. portraitImg);
    RAC(self. userNameLabel,  text) = RACObserve(self,  viewModel. userName);
}
  • ViewModel
提供了这个页面展示所有需要的数据的一个对象。

举一个简单的例子:


@interface UserListViewModel: NSObject
@property (nonatomic, assign, readonly) BOOL loading;
@property (nonatomic, strong, readonly) NSArray <UserListModel *>*userList;
@property (nonatomic, strong, readwrite) NSString *searchUserName;
  
- (void) searchUser;
- (void) deleteUserWithModel:(UserListModel *)model;
- (void) loadMoreUser;

这个ViewModel里涵盖了所有页面展示需要的要素。用户列表、搜索名称、是否需要显示网络加载的小菊花。
并且涵盖了对这些数据的所有操作方法。加载更多、搜索、删除。
但是、ViewModel到底也是一个Model层、不应该引入UIKit(View层)。如果删除需要弹窗、那么这个弹窗动作是不应该交给ViewModel来搞的、因为这已经不属于数据处理的范畴了
仔细想想、这个ViewModel其实就是把Controller中与页面相关的数据处理代码挪进来了而已。
如此、我们设置可以脱离View层。拿着这个ViewModel去跑单元测试。简直碉堡。

  • Controller
    虽然MVVM中没有体现出C的字眼、但是实际操作肯定是要遵循着View <-> C <-> ViewModel <-> Model
    起码在iOS中是、这和Vue中简单粗暴的方式不同:
//页面里
<p>{{message}}</p>
<li v-for="value in arr">
       {{value}}
</li>

//js文件里
new Vue({
//数据
       data:{
             key:'welcome vue',
             arr:['apple','banana','orange','pear'],
             json:{a:'apple',b:'banana',c:'orange'}
       }
      //方法
      methods:{
            add:function(){
            //push 添加元素
                  this.arr.push('tomato');
            }
      }
})

因为Html中并没有明确的Controller的概念、整个Html文件就是Controller容器。
和iOS的区别很明显:
除去精炼的写法、整个Html文件所关联的js资源都可以无障碍互通、所以View层无时无刻不持有着Model层、在View层直接绑定更方便。

  • 所以iOS中Controller的作用就显而易见了
    Controller夹在ViewViewModel之间做的其中一个主要事情就是将ViewViewModel进行绑定。在逻辑上、Controller知道应当展示哪个ViewController也知道应当使用哪个ViewModel、然而ViewViewModel它们之间是互相不知道的、所以Controller就负责控制他们的绑定关系。

  • ReactiveCocoa对于MVVM的意义是什么?

ReactiveCocoa并不是MVVM思想的根本、不用ReactiveCocoa也能MVVM、用ReactiveCocoa能更好地体现MVVM的精髓。
我一直强调MVC中的MV是应该尽量不要互相持有的。
这个时候如何把原本松散的二者通过C紧密的联系起来、就要进行数据绑定。
而这种数据绑定、iOS本身并没有什么太靠谱的办法(就像刚才前端例子中的<p>{{message}}</p>、这种写法)。

虽然KVO、Notification、block、delegate和target-action都可以用来做数据通信进而实现绑定

但都不如ReactiveCocoa来的《《《优雅》》》。

对、这就是我开始说为什么RAC对于MVVM不是必须的。
如果不用ReactiveCocoa、绑定关系可能就做不到那么松散那么好、但并不影响它还是MVVM。


MVCS

将数据持久化的代码移植给了store...


MVP

实际上就是将Controller中关于ModelView的调配处理的代码移植了过来。各部分分工如下:

  • View
    负责界面展示和布局管理、向Presenter暴露视图更新和数据获取的接口
  • Presenter
    负责接收来自View的事件、通过View提供的接口更新视图,并管理Model。
  • Model
    和MVC中的一样,提供数据模型

VIPER

除了View没拆、其它的都拆了....
在MVP的基础上新增了Interactor与Router

  • View
    • 提供完整的视图。负责视图的组合、布局、更新
    • 向Presenter提供更新视图的接口
    • 将View相关的事件发送给Presenter
  • Interactor
    • 维护主要的业务逻辑功能,向Presenter提供现有的业务用例
    • 维护、获取、更新Entity
    • 当有业务相关的事件发生时、处理事件、并通知Presenter
  • Presenter
    • 接收并处理来自View的事件
    • 向Interactor请求调用业务逻辑
    • 向Interactor提供View中的数据
    • 接收并处理来自Interactor的数据回调事件
    • 通知View进行更新操作
    • 通过Router跳转到其他View
  • Entity
    • 和Model一样的数据模型
  • Router
    • 提供View之间的跳转功能、减少了模块间的耦合
    • 初始化VIPER的各个模块

VIPER与其他的架构相比最大的优势就是粒度简直细化成了尘埃、极大的提高了可测性。
但问题也相当显著、层级越多、数据传递的工作量(API)就越大。文件越多、新建一个页面的成本也就越高。


关于架构设计、一些观点

除了MVVM、它对iOS的入侵性简直太高、主要取决于团队的决策(比如是不是新项目、Leader是不是想玩玩看)。

  • 控制好Controller的代码量

随着项目的进行、代码量最多只能优化、膨胀不可避免。
而在没办法继续精简的前提下、想控制Controller的代码量。就要在可读性和通用性之间进行取舍。该挪走的时候就挪走吧、毕竟梳理一个单独的模块、比梳理一个几千行的Controller要方便多了。

  • 对于MVX如何选择
    • 其实完全要看业务的性质以及复杂度。
    • 如果你一个页面只有一个UITableView、搞出一些奇淫技巧其实意义 不大、徒增烦恼。踏踏实实用MVC对大家都好。
    • 如果感觉业务里有非常多的View与Model互通、或者需大量复用、可以用MVP。
    • 如果有大量的数据读写、可以用MVCS。
    • 如果业务相当的复杂、耦合让人浑身难受。做好模块化或者干脆VIPER才是出路。
  • 无论用哪种模式、都要深刻的理解每个模块不同的职责

比如MVVM里的VM、既然是Model层、就不要把UIKit放进去。
再比如MVP中的P、既然是为了帮助Controller协调M与V而生、就不要把与二者无关的工作也抢过来干。

参考资料

(要感谢评论区SlowMaker的提醒)
写的时候主要的结构思路借鉴了两年前入行时看过的一篇博客《《iOS应用架构谈 view层的组织和调用方案》》、对其中自己认为不太好理解的部分进行了补充和扩展。
也推荐一下大神微博:@反革命攻城狮CasaTaloyum
很推荐去搜藏并且拜读一下CasaTaloyum的博客、会让你对架构整体的把控有一个质的提升。

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

推荐阅读更多精彩内容