分享:iOS应用架构之谈谈View层的组织和调用方案(一)

322 阅读31分钟

当我们开始设计View层的架构时,往往是这个App还没有开始开发,或者这个App已经发过几个版本了,然后此时需要做非常彻底的重构。

一般也就是这两种时机会去做View层架构,基于这个时机的特殊性,我们在这时候必须清楚认识到:View层的架构一旦实现或定型,在App发版后可修改的余地就已经非常之小了。因为它跟业务关联最为紧密,所以哪怕稍微动一点点,它所引发的蝴蝶效应都不见得是业务方能够hold住的。这样的情况,就要求我们在实现这个架构时,代码必须得改得勤快,不能偷懒。也必须抱着充分的自我怀疑态度,做决策时要拿捏好尺度。

View层的架构非常之重要,为什么这么说?

View层架构是影响业务方迭代周期的因素之一

产品经理产生需求的速度会非常快,尤其是公司此时仍处于创业初期,在规模稍大的公司里面,产品经理也喜欢挖大坑来在leader面前刷存在感,比如阿里。这就导致业务工程师任务非常繁重。正常情况下让产品经理砍需求是不太可能的,因此作为架构师,在架构里有一些可做可不做的事情,最好还是能做就做掉,不要偷懒。这可以帮业务方减负,编写代码的时候也能更加关注业务。

我跟一些朋友交流的时候,他们都会或多或少地抱怨自己的团队迭代速度不够快,或者说,迭代速度不合理地慢。我认为迭代速度不是想提就能提的,迭代速度的影响因素有很多,一期PRD里的任务量和任务复杂度都会影响迭代周期能达到什么样的程度。抛开这些外在的不谈,从内在可能导致迭代周期达不到合理的速度的原因来看,其中有一个原因很有可能就是View层架构没有做好,让业务工程师完成一个不算复杂的需求时,需要处理太多额外的事情。当然,开会多,工程师水平烂也属于迭代速度提不上去的内部原因,但这个不属于本文讨论范围。还有,加班不是优化迭代周期的正确方式,嗯。

一般来说,一个不够好的View层架构,主要原因有以下五种:

  1. 代码混乱不规范
  2. 过多继承导致的复杂依赖关系
  3. 模块化程度不够高,组件粒度不够细
  4. 横向依赖
  5. 架构设计失去传承

这五个地方会影响业务工程师实现需求的效率,进而拖慢迭代周期。View架构的其他缺陷也会或多或少地产生影响,但在我看来这里五个是比较重要的影响因素。如果大家觉得还有什么因素比这四个更高的,可以在评论区提出来我补上去。

对于第五点我想做一下强调:架构的设计是一定需要有传承的,有传承的架构从整体上看会非常协调。但实际情况有可能是一个人走了,另一个顶上,即便任务交接得再完整,都不可避免不同的人有不同的架构思路,从而导致整个架构的流畅程度受到影响。要解决这个问题,一方面要尽量避免单点问题,让架构师做架构的时候再带一个人。另一方面,架构要设计得尽量简单,平缓接手人的学习曲线。

所以当各位CTO、技术总监、TeamLeader们觉得迭代周期不够快时,你可以先不忙着急吼吼地去招新人,《人月神话》早就说过加人不能完全解决问题。这时候如果你可以回过头来看一下是不是View层架构不合理,把这个弄好也是优化迭代周期的手段之一。

View层架构是最贴近业务的底层架构

View层架构虽然也算底层,但还没那么底层,它跟业务的对接面最广,影响业务层代码的程度也最深。在所有的底层都牵一发的时候,在View架构上牵一发导致业务层动全身的面积最大。

所以View架构在所有架构中一旦定型,可修改的空间就最小,我们在一开始考虑View相关架构时,不光要实现功能,还要考虑更多规范上的东西。制定规范的目的一方面是防止业务工程师的代码腐蚀View架构,另一方面也是为了能够有所传承。按照规范来,总还是不那么容易出差池的。

还有就是,架构师一开始考虑的东西也会有很多,不可能在第一版就把它们全部实现,对于一个尚未发版的App来说,第一版架构往往是最小完整功能集,那么在第二版第三版的发展过程中,架构的迭代任务就很有可能不只是你一个人的事情了,相信你一个人也不见得能搞定全部。所以你要跟你的合作者们有所约定。另外,第一版出去之后,业务工程师在使用过程中也会产生很多修改意见,哪些意见是合理的,哪些意见是不合理的,也要通过事先约定的规范来进行筛选,最终决定如何采纳。

规范也不是一成不变的,什么时候枪毙意见,什么时候改规范,这就要靠各位的技术和经验了。


以上就是前言。

这篇文章讲什么?

  • View代码结构的规定
  • 关于view的布局
  • 何时使用storyboard,何时使用nib,何时使用代码写View
  • 是否有必要让业务方统一派生ViewController?
  • 方便View布局的小工具
  • MVC、MVVM、MVCS、VIPER
  • 本门心法
  • 跨业务时View的处理
  • 留给评论区各种补
  • 总结

View代码结构的规定

架构师不是写SDK出来交付业务方使用就没事儿了的,每家公司一定都有一套代码规范,架构师的职责也包括定义代码规范。按照道理来讲,定代码规范应该是属于通识,放在这里讲的原因只是因为我这边需要为View添加一个规范。

制定代码规范严格来讲不属于View层架构的事情,但它对View层架构未来的影响会比较大,也是属于架构师在设计View层架构时需要考虑的事情。制定View层规范的重要性在于:

  1. 提高业务方View层的可读性可维护性
  2. 防止业务代码对架构产生腐蚀
  3. 确保传承
  4. 保持架构发展的方向不轻易被不合理的意见所左右

在这一节里面我不打算从头开始定义一套规范,苹果有一套Coding Guidelines,当我们定代码结构或规范的时候,首先一定要符合这个规范。

然后,相信大家各自公司里面也都有一套自己的规范,具体怎么个规范法其实也是根据各位架构师的经验而定,我这边只是建议各位在各自规范的基础上再加上下面这一点。

viewController的代码应该差不多是这样:

image.png

要点如下:

所有的属性都使用getter和setter

不要在viewDidLoad里面初始化你的view然后再add,这样代码就很难看。在viewDidload里面只做addSubview的事情,然后在viewWillAppear里面做布局的事情(勘误1),最后在viewDidAppear里面做Notification的监听之类的事情。至于属性的初始化,则交给getter去做。

比如这样:

#pragma mark - life cycle
- (void)viewDidLoad
{
    [super viewDidLoad];

    self.view.backgroundColor = [UIColor whiteColor];
    [self.view addSubview:self.firstTableView];
    [self.view addSubview:self.secondTableView];
    [self.view addSubview:self.firstFilterLabel];
    [self.view addSubview:self.secondFilterLabel];
    [self.view addSubview:self.cleanButton];
    [self.view addSubview:self.originImageView];
    [self.view addSubview:self.processedImageView];
    [self.view addSubview:self.activityIndicator];
    [self.view addSubview:self.takeImageButton];
}

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];

    CGFloat width = (self.view.width - 30) / 2.0f;

    self.originImageView.size = CGSizeMake(width, width);
    [self.originImageView topInContainer:70 shouldResize:NO];
    [self.originImageView leftInContainer:10 shouldResize:NO];

    self.processedImageView.size = CGSizeMake(width, width);
    [self.processedImageView right:10 FromView:self.originImageView];
    [self.processedImageView topEqualToView:self.originImageView];

    CGFloat labelWidth = self.view.width - 100;
    self.firstFilterLabel.size = CGSizeMake(labelWidth, 20);
    [self.firstFilterLabel leftInContainer:10 shouldResize:NO];
    [self.firstFilterLabel top:10 FromView:self.originImageView];

    ... ...
}

这样即便在属性非常多的情况下,还是能够保持代码整齐,view的初始化都交给getter去做了。总之就是尽量不要出现以下的情况:

- (void)viewDidLoad
{
    [super viewDidLoad];

    self.textLabel = [[UILabel alloc] init];
    self.textLabel.textColor = [UIColor blackColor];
    self.textLabel ... ...
    self.textLabel ... ...
    self.textLabel ... ...
    [self.view addSubview:self.textLabel];
}

这种做法就不够干净,都扔到getter里面去就好了。关于这个做法,在唐巧的技术博客里面有一篇文章和我所提倡的做法不同,这个我会放在后面详细论述。

getter和setter全部都放在最后

因为一个ViewController很有可能会有非常多的view,就像上面给出的代码样例一样,如果getter和setter写在前面,就会把主要逻辑扯到后面去,其他人看的时候就要先划过一长串getter和setter,这样不太好。然后要求业务工程师写代码的时候按照顺序来分配代码块的位置,先是life cycle,然后是Delegate方法实现,然后是event response,然后才是getters and setters。这样后来者阅读代码时就能省力很多。

每一个delegate都把对应的protocol名字带上,delegate方法不要到处乱写,写到一块区域里面去

比如UITableViewDelegate的方法集就老老实实写上#pragma mark - UITableViewDelegate。这样有个好处就是,当其他人阅读一个他并不熟悉的Delegate实现方法时,他只要按住command然后去点这个protocol名字,Xcode就能够立刻跳转到对应这个Delegate的protocol定义的那部分代码去,就省得他到处找了。

event response专门开一个代码区域

所有button、gestureRecognizer的响应事件都放在这个区域里面,不要到处乱放。

关于private methods,正常情况下ViewController里面不应该写

不是delegate方法的,不是event response方法的,不是life cycle方法的,就是private method了。对的,正常情况下ViewController里面一般是不会存在private methods的,这个private methods一般是用于日期换算、图片裁剪啥的这种小功能。这种小功能要么把它写成一个category,要么把他做成一个模块,哪怕这个模块只有一个函数也行。

ViewController基本上是大部分业务的载体,本身代码已经相当复杂,所以跟业务关联不大的东西能不放在ViewController里面就不要放。另外一点,这个private method的功能这时候只是你用得到,但是将来说不定别的地方也会用到,一开始就独立出来,有利于将来的代码复用。

为什么要这样要求?

我见过无数ViewController,代码布局乱得一塌糊涂,这里一个delegate那里一个getter,然后ViewController的代码一般都死长死长的,看了就让人头疼。

定义好这个规范,就能使得ViewController条理清晰,业务方程序员很能够区分哪些放在ViewController里面比较合适,哪些不合适。另外,也可以提高代码的可维护性和可读性。

关于View的布局

业务工程师在写View的时候一定逃不掉的就是这个命题。用Frame也好用Autolayout也好,如果没有精心设计过,布局部分一定惨不忍睹。

直接使用CGRectMake的话可读性很差,光看那几个数字,也无法知道view和view之间的位置关系。用Autolayout可读性稍微好点儿,但生成Constraint的长度实在太长,代码观感不太好。

Autolayout这边可以考虑使用Masonry,代码的可读性就能好很多。如果还有使用Frame的,可以考虑一下使用这个项目

这个项目里面提供了Frame相关的方便方法(UIView+LayoutMethods),里面的方法也基本涵盖了所有布局的需求,可读性非常好,使用它之后基本可以和CGRectMake说再见了。

这个项目也提供了Autolayout方案下生产Constraints的方便方法(UIView+AEBHandyAutoLayout),可读性比原生好很多。我当时在写这系列方法的时候还不知道有Masonry。知道有Masonry之后我特地去看了一下,发现Masonry功能果然强大。不过这系列方法虽然没有Masonry那么强大,但是也够用了。当时安居客iPad版App全部都是Autolayout来做的View布局,就是使用的这个项目里面的方法。可读性很好。

让业务工程师使用良好的工具来做View的布局,能提高他们的工作效率,也能减少bug发生的几率。架构师不光要关心那些高大上的内容,也要多给业务工程师提供方便易用的小工具,才能发挥架构师的价值。

在这里我还想补充一些内容:

具有一定规模的团队化iOS开发(10人以上)有以下几个特点:

  1. 同一份代码文件的作者会有很多,不同作者同时修改同一份代码的情况也不少见。因此,使用Git进行代码版本管理时出现Conflict的几率也比较大。
  2. 需求变化非常频繁,产品经理一时一个主意,为了完成需求而针对现有代码进行微调的情况,以及针对现有代码的部分复用的情况也比较多。
  3. 复杂界面元素、复杂动画场景的开发任务比较多。

如果这三个特点你一看就明白了,下面的解释就可以不用看了。如果你针对我的倾向愿意进一步讨论的,可以先看我下面的解释,看完再说。

同一份代码文件的作者会有很多,不同作者同时修改同一份代码的情况也不少见。因此,使用Git进行代码版本管理时出现Conflict的几率也比较大。

iOS开发过程中,会遇到最蛋疼的两种Conflict一个是project.pbxproj,另外一个就是StoryBoardXIB。因为这些文件的内容的可读性非常差,虽然苹果在XCode5(现在我有点不确定是不是这个版本了)中对StoryBoard的文件描述方式做了一定的优化,但只是把可读性从非常差提升为很差

然而在StoryBoard中往往包含了多个页面,这些页面基本上不太可能都由一个人去完成,如果另一个人在做StoryBoard的操作的时候,出于某些目的动了一下不属于他的那个页面,比如为了美观调整了一下位置。然后另外一个人也因为要添加一个页面,而在Storyboard中调整了一下某个其他页面的位置。那么针对这个情况我除了说个呵呵以外,我就只能说:祝你好运。看清楚哦,这还没动具体的页页面内容呢。

但如果使用代码绘制View,Conflict一样会发生,但是这种Conflict就好解很多了,你懂的。

需求变化非常频繁,产品经理一时一个主意,为了完成需求而针对现有代码进行微调的情况,以及针对现有代码的部分复用的情况也比较多。

我觉得产品经理一时一个主意不是他的错,他说不定也是被逼的,比如谁都会来掺和一下产品的设计,公司里的所有人,上至CEO,下至基层员工都有可能对产品设计评头论足,只要他个人有个地方用得不爽(极大可能是个人喜好)然后又正好跟产品经理比较熟悉能够搭得上话,都会提出各种意见。产品经理躲不起也惹不起,有时也是没办法,嗯。

但落实到工程师这边来,这种情况就很蛋疼。因为这种改变有时候不光是UI,UI所对应的逻辑也有要改的可能,工程师就会两边文件都改,你原来link的那个view现在不link了,然后你的outlet对应也要删掉,这两部分只要有一个没做,编译通过之后跑一下App,一会儿就crash了。看起来这不是什么大事儿,但很影响心情。

另外,如果出现部分的代码复用,比如说某页面下某个View也希望放在另外一个页面里,相关的操作就不是复制粘贴这么简单了,你还得重新link一遍。也很影响心情。

复杂界面元素,复杂动画交互场景的开发任务比较多。

要是想在基于StoryBoard的项目中做一个动画,很烦。做几个复杂界面元素,也很烦。有的时候我们挂Custom View上去,其实在StoryBoard里面看来就是一个空白View。然后另外一点就是,当你的layout出现问题需要调整的时候,还是挺难找到问题所在的,尤其是在复杂界面元素的情况下。

所以在针对View层这边的要求时,我也是建议不要用StoryBoard。实现简单的东西,用Code一样简单,实现复杂的东西,Code比StoryBoard更简单。所以我更加提倡用code去画view而不是storyboard。

是否有必要让业务方统一派生ViewController

有的时候我们出于记录用户操作行为数据的需要,或者统一配置页面的目的,会从UIViewController里面派生一个自己的ViewController,来执行一些通用逻辑。比如天猫客户端要求所有的ViewController都要继承自TMViewController。这个统一的父类里面针对一个ViewController的所有生命周期都做了一些设置,至于这里都有哪些设置对于本篇文章来说并不重要。在这里我想讨论的是,在设计View架构时,如果为了能够达到统一设置或执行统一逻辑的目的,使用派生的手段是有必要的吗?

我觉得没有必要,为什么没有必要?

  1. 使用派生比不使用派生更容易增加业务方的使用成本
  2. 不使用派生手段一样也能达到统一设置的目的

这两条原因是我认为没有必要使用派生手段的理由,如果两条理由你都心领神会,那么下面的就可以不用看了。如果你还有点疑惑,请看下面我来详细讲一下原因。

为什么使用了派生,业务方的使用成本会提升?

其实不光是业务方的使用成本,架构的维护成本也会上升。那么具体的成本都来自于哪里呢?

  • 集成成本

这里讲的集成成本是这样的:如果业务方自己开了一个独立demo,快速完成了某个独立流程,现在他想把这个现有流程集合进去。那么问题就来了,他需要把所有独立的UIViewController改变成TMViewController。那为什么不是一开始就立刻使用TMViewController呢?因为要想引入TMViewController,就要引入整个天猫App所有的业务线,所有的基础库,因为这个父类里面涉及很多天猫环境才有的内容,所谓拔出萝卜带出泥,你要是想简单继承一下就能搞定的事情,搭环境就要搞半天,然后这个小Demo才能跑得起来。

对于业务层存在的所有父类来说,它们是很容易跟项目中的其他代码纠缠不清的,这使得业务方开发时遇到一个两难问题:要么把所有依赖全部搞定,然后基于App环境(比如天猫)下开发Demo要么就是自己Demo写好之后,按照环境要求改代码。这里的两难问题都会带来成本,都会影响业务方的迭代进度。

我不确定各位所在公司是否会有这样的情况,但我可以在这里给大家举一个阿里的真实的例子:在开发某滤镜Demo和相关页面流程,最终是要合并到天猫这个App里面去的。使用天猫环境进行开发的话,pod install完所有依赖差不多需要10分钟,然后打开workspace之后,差不多要再等待1分钟让xcode做好索引,然后才能正式开始工作。但如果天猫环境有更新,你就要再重复一次上面的流程,否则 就很有可能编译不过。

拜托,我只是想做个Demo而已,不想搞那么复杂。

  • 上手接受成本

新来的业务工程师有的时候不见得都记得每一个ViewController都必须要派生自TMViewController而不是直接的UIViewController。新来的工程师他不能直接按照苹果原生的做法去做事情,他需要额外学习,比如说:所有的ViewController都必须继承自TMViewController。

  • 架构的维护难度

尽可能少地使用继承能提高项目的可维护性,具体内容在《跳出面向对象思想(一) 继承》里面说了,在这里想偷懒不想把那篇文章里说过的东西再说一遍。

其实对于业务方来说,主要还是第一个集成成本比较蛋疼,因为这是长痛,每次要做点什么事情都会遇到。第二点倒还好,短痛。第三点跟业务工程师没啥关系。

那么如果不使用派生,我们应该使用什么手段?

我的建议是使用AOP。

在架构师实现具体的方案之前,必须要想清楚几个问题,然后才能决定采用哪种方案。是哪几个问题?

  1. 方案的效果,和最终要达到的目的是什么?
  2. 在自己的知识体系里面,是否具备实现这个方案的能力?
  3. 在业界已有的开源组件里面,是否有可以直接拿来用的轮子?

这三个问题按照顺序一一解答之后,具体方案就能出来了。

我们先看第一个问题:方案的效果,和最终要达到的目的是什么?

方案的效果应该是:

  1. 业务方可以不用通过继承的方法,然后框架能够做到对ViewController的统一配置。
  2. 业务方即使脱离框架环境,不需要修改任何代码也能够跑完代码。业务方的ViewController一旦丢入框架环境,不需要修改任何代码,框架就能够起到它应该起的作用。

其实就是要实现不通过业务代码上对框架的主动迎合,使得业务能够被框架感知这样的功能。细化下来就是两个问题,框架要能够拦截到ViewController的生命周期,另一个问题就是,拦截的定义时机。

对于方法拦截,很容易想到Method Swizzling,那么我们可以写一个实例,在App启动的时候添加针对UIViewController的方法拦截,这是一种做法。还有另一种做法就是,使用NSObject的load函数,在应用启动时自动监听。使用后者的好处在于,这个模块只要被项目包含,就能够发挥作用,不需要在项目里面添加任何代码。

然后另外一个要考虑的事情就是,原有的TMViewController(所谓的父类)也是会提供额外方法方便子类使用的,Method Swizzling只支持针对现有方法的操作,拓展方法的话,嗯,当然是用Category啦。

我本人不赞成Category的过度使用,但鉴于Category是最典型的化继承为组合的手段,在这个场景下还是适合使用的。还有的就是,关于Method Swizzling手段实现方法拦截,业界也已经有了现成的开源库:Aspects,我们可以直接拿来使用。

然后另外要提醒的是,这方案的目的是消除不必要的继承,虽然不限定于UIViewController,但它也是有适用范围的,在适用继承的地方,还是要老老实实使用继承。比如你有一个数据模型,是由基本模型派生出的一整套模型,那么这个时候还是老老实实使用继承。至于拿捏何时使用继承,相信各位架构师一定能够处理好.

关于MVC、MVVM等一大堆思想

其实这些都是相对通用的思想,万变不离其宗的还是在开篇里面我提到的那三个角色:数据管理者数据加工者数据展示者。这些五花八门的思想,不外乎就是制订了一个规范,规定了这三个角色应当如何进行数据交换。但同时这些也是争议最多的话题,所以我在这里来把几个主流思想做一个梳理,当你在做View层架构时,能够有个比较好的参考。

MVC

MVC(Model-View-Controller)是最老牌的的思想,老牌到4人帮的书里把它归成了一种模式,其中Model就是作为数据管理者View作为数据展示者Controller作为数据加工者ModelView又都是由Controller来根据业务需求调配,所以Controller还负担了一个数据流调配的功能。

在iOS开发领域,我们应当如何进行MVC的划分?

这里面其实有两个问题:

  1. 为什么我们会纠结于iOS开发领域中MVC的划分问题?
  2. 在iOS开发领域中,怎样才算是划分的正确姿势?

为什么我们会纠结于iOS开发领域中MVC的划分问题?

关于这个,每个人纠结的点可能不太一样,我也不知道大家的观点。但请允许我猜一下:是不是因为UIViewController中自带了一个View,且控制了View的整个生命周期(viewDidLoad,viewWillAppear...),而在常识中我们都知道Controller不应该和View有如此紧密的联系,所以才导致大家对划分产生困惑?,下面我会针对这个猜测来给出我的意见。

在服务端开发领域,Controller和View的交互方式一般都是这样,比如Yii:

    /*
        ...
            数据库取数据
        ...
            处理数据
        ...
    */

    // 此处$this就是Controller
    $this->render("plan",array(
        'planList' => $planList,
        'plan_id' => $_GET['id'],
    ));

这里Controller和View之间区分得非常明显,Controller做完自己的事情之后,就把所有关于View的工作交给了页面渲染引擎去做,Controller不会去做任何关于View的事情,包括生成View,这些都由渲染引擎代劳了。这是一个区别,但其实服务端View的概念和Native应用View的概念,真正的区别在于:从概念上严格划分的话,服务端其实根本没有View,拜HTTP协议所赐,我们平时所讨论的View只是用于描述View的字符串(更实质的应该称之为数据),真正的View是浏览器。

所以服务端只管生成对View的描述,至于对View的长相,UI事件监听和处理,都是浏览器负责生成和维护的。但是在Native这边来看,原本属于浏览器的任务也逃不掉要自己做。那么这件事情由谁来做最合适?苹果给出的答案是:UIViewController

鉴于苹果在这一层做了很多艰苦卓绝的努力,让iOS工程师们不必亲自去实现这些内容。而且,它把所有的功能都放在了UIView上,并且把UIView做成不光可以展示UI,还可以作为容器的一个对象。

看到这儿你明白了吗?UIView的另一个身份其实是容器!UIViewController中自带的那个view,它的主要任务就是作为一个容器。如果它所有的相关命名都改成ViewContainer,那么代码就会变成这样:

- (void)viewContainerDidLoad
{
    [self.viewContainer addSubview:self.label];
    [self.viewContainer addSubview:self.tableView];
    [self.viewContainer addSubview:self.button];
    [self.viewContainer addSubview:self.textField];
}

... ...

仅仅改了个名字,现在是不是感觉清晰了很多?如果再要说详细一点,我们平常所认为的服务端MVC是这样划分的:

               ---------------------------
               | C                       |
               |        Controller       |
               |                         |
               ---------------------------
              /                           \
             /                             \
            /                               \
------------                                 ---------------------
| M        |                                 | V                 |
|   Model  |                                 |    Render Engine  |
|          |                                 |          +        |
------------                                 |      HTML Files   |
                                             ---------------------

但事实上,整套流程的MVC划分是这样:

               ---------------------------
               | C                       |
               |   Controller            |
               |           \             |
               |           Render Engine |
               |                 +       |
               |             HTML Files  |
               ---------------------------
              /                           \
             /                             \ HTML String
            /                               \
------------                                 ---------------
| M        |                                 | V           |
|   Model  |                                 |    Browser  |
|          |                                 |             |
------------                                 ---------------

由图中可以看出,我们服务端开发在这个概念下,其实只涉及M和C的开发工作,浏览器作为View的容器,负责View的展示和事件的监听。那么对应到iOS客户端的MVC划分上面来,就是这样:

               ----------------------------
               | C                        |
               |   Controller             |
               |           \              |
               |           View Container |
               ----------------------------
              /                            \
             /                              \
            /                                \
------------                                  ----------------------
| M        |                                  | V                  |
|   Model  |                                  |    UITableView     |
|          |                                  |    YourCustomView  |
------------                                  |         ...        |
                                              ----------------------

唯一区别在于,View的容器在服务端,是由Browser负责,在整个网站的流程中,这个容器放在Browser是非常合理的。在iOS客户端,View的容器是由UIViewController中的view负责,我也觉得苹果做的这个选择是非常正确明智的。

因为浏览器和服务端之间的关系非常松散,而且他们分属于两个不同阵营,服务端将对View的描述生成之后,交给浏览器去负责展示,然而一旦view上有什么事件产生,基本上是很少传递到服务器(也就是所谓的Controller)的(要传也可以:AJAX),都是在浏览器这边把事情都做掉,所以在这种情况下,View容器就适合放在浏览器(V)这边。

但是在iOS开发领域,虽然也有让View去监听事件的做法,但这种做法非常少,都是把事件回传给Controller,然后Controller再另行调度。所以这时候,View的容器放在Controller就非常合适。Controller可以因为不同事件的产生去很方便地更改容器内容,比如加载失败时,把容器内容换成失败页面的View,无网络时,把容器页面换成无网络的View等等。

在iOS开发领域中,怎样才算是MVC划分的正确姿势?

这个问题其实在上面已经解答掉一部分了,那么这个问题的答案就当是对上面问题的一个总结吧。

M应该做的事:

  1. 给ViewController提供数据
  2. 给ViewController存储数据提供接口
  3. 提供经过抽象的业务基本组件,供Controller调度

C应该做的事:

  1. 管理View Container的生命周期
  2. 负责生成所有的View实例,并放入View Container
  3. 监听来自View与业务有关的事件,通过与Model的合作,来完成对应事件的业务。

V应该做的事:

  1. 响应与业务无关的事件,并因此引发动画效果,点击反馈(如果合适的话,尽量还是放在View去做)等。
  2. 界面元素表达

通过与服务端MVC划分的对比来回答了这两个问题,之所以这么做,是因为我知道有很多iOS工程师之前是从服务端转过来的。

MVCS

苹果自身就采用的是这种架构思路,从名字也能看出,也是基于MVC衍生出来的一套架构。从概念上来说,它拆分的部分是Model部分,拆出来一个Store。这个Store专门负责数据存取。但从实际操作的角度上讲,它拆开的是Controller。

这算是瘦Model的一种方案,瘦Model只是专门用于表达数据,然后存储、数据处理都交给外面的来做。MVCS使用的前提是,它假设了你是瘦Model,同时数据的存储和处理都在Controller去做。所以对应到MVCS,它在一开始就是拆分的Controller。因为Controller做了数据存储的事情,就会变得非常庞大,那么就把Controller专门负责存取数据的那部分抽离出来,交给另一个对象去做,这个对象就是Store。这么调整之后,整个结构也就变成了真正意义上的MVCS。

关于胖Model和瘦Model

我发现知道胖Model和瘦Model的概念的人不是很多。国外业界曾经对此有过非常激烈的讨论,主题就是Fat model, skinny controller。哪个更好,业界也还没有定论,所以这算是目前业界悬而未解的一个争议。我很少看到国内有讨论这个的资料,所以在这里我打算补充一下什么叫胖Model什么叫瘦Model。以及他们的争论来源于何处。

  • 什么叫胖Model?

胖Model包含了部分弱业务逻辑。胖Model要达到的目的是,Controller从胖Model这里拿到数据之后,不用额外做操作或者只要做非常少的操作,就能够将数据直接应用在View上。举个例子:

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];

把timestamp转换成具体业务上所需要的字符串,这属于业务代码,算是弱业务。FatModel做了这些弱业务之后,Controller就能变得非常skinny,Controller只需要关注强业务代码就行了。众所周知,强业务变动的可能性要比弱业务大得多,弱业务相对稳定,所以弱业务塞进Model里面是没问题的。另一方面,弱业务重复出现的频率要大于强业务,对复用性的要求更高,如果这部分业务写在Controller,类似的代码会洒得到处都是,一旦弱业务有修改(弱业务修改频率低不代表就没有修改),这个事情就是一个灾难。如果塞到Model里面去,改一处很多地方就能跟着改,就能避免这场灾难。

然而其缺点就在于,胖Model相对比较难移植,虽然只是包含弱业务,但好歹也是业务,迁移的时候很容易拔出萝卜带出泥。另外一点,MVC的架构思想更加倾向于Model是一个Layer,而不是一个Object,不应该把一个Layer应该做的事情交给一个Object去做。最后一点,软件是会成长的,FatModel很有可能随着软件的成长越来越Fat,最终难以维护。

  • 什么叫瘦Model? 瘦Model只负责业务数据的表达,所有业务无论强弱一律扔到Controller。瘦Model要达到的目的是,尽一切可能去编写细粒度Model,然后配套各种helper类或方法来对弱业务做抽象,强业务依旧交给Controller。举个例子:
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) {
        ...
    }

由于SlimModel跟业务完全无关,它的数据可以交给任何一个能处理它数据的Helper或其他的对象,来完成业务。在代码迁移的时候独立性很强,很少会出现拔出萝卜带出泥的情况。另外,由于SlimModel只是数据表达,对它进行维护基本上是0成本,软件膨胀得再厉害,SlimModel也不会大到哪儿去。

缺点就在于,Helper这种做法也不见得很好,批判了这个事情。另外,由于Model的操作会出现在各种地方,SlimModel在一定程度上违背了DRY(Don't Repeat Yourself)的思路,Controller仍然不可避免在一定程度上出现代码膨胀。

我的态度?嗯,我会在本门心法这一节里面说。

说回来,MVCS是基于瘦Model的一种架构思路,把原本Model要做的很多事情中的其中一部分关于数据存储的代码抽象成了Store,在一定程度上降低了Controller的压力。

小总结

未完待续...

分享:iOS开发各类书籍资料下载

收录自|地址