说是读后感,根本算不上,当记个笔记,划个重点吧。

Reference

前言

View 层的架构一旦实现或者定型,发版之后可修改的余地就已经非常之小了。

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

不够好的 View 层架构,主要原因

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

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

View 层跟业务的对接面最广,影响业务层代码程度也最深,牵一发动全身。

一旦成型,可修改空间就最小,制定规范一方面是防止业务工程师的代码腐蚀 View 架构,另一方面也是为了能够有所传承。

规范也不是一沉不变的,枪毙意见还是修改规范,这就要靠各位技术和经验了。

View 代码结构的规定

定代码规范是通识!

重要性

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

要点

  1. 所有的属性都使用 getter 和 setter
  2. getter 和 setter 全部都放在最后
  3. 每一个 delegate 都把对应的 protocol 名字带上,delegate 方法不要到处乱写,写到一块区域里面去
  4. event response 专门开一个代码区域
  5. private methods,正常情况下 ViewController 里面不应该写

定义好规范,使得 ViewControllert 条理清晰,业务工程师能够区分哪些放在 ViewController 里面比较合适,也可以提高代码的可维护性和可读性。

关于 View 的布局

  1. Frame: pod’HandyFrame
  2. AutoLayout: pod’Masonry

增加可读性,提高工作效率。

哪里写 Constraints?

viewDidLoad 里面自己开一个 layoutPageSubviews 方法在里面创建 Constrains 并添加,较好。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

- (void)viewDidLoad
{
[super viewDidLoad];

[self.view addSubview:self.firstView];
[self.view addSubview:self.secondView];
[self.view addSubview:self.thirdView];

[self layoutPageSubviews];
}

- (void)layoutPageSubviews
{
[self.view addConstraints:xxxConstraints];
[self.view addConstraints:yyyConstraints];
[self.view addConstraints:zzzConstraints];
}

viewWillAppear 这里改变UI元素不是很可靠。

AutoLayout 发生在 viewWillAppear 之后,严格来说这里通常不做视图位置的修改,而用来更新 Form 数据。

改变位置可以放在 viewWillLayoutSubviews 或者 viewDidLayoutSubviews 里,而且在 viewDidLayoutSubviews 确定 UI 位置关系之后设置 AutoLayout 比较稳妥。

另外,viewWillAppear 在每次页面即将显示都会调用,viewWillLayoutSubviews 虽然在 lifecycle 里调用顺序在 viewWillAppear 之后,但是只有在页面元素需要调整时才会调用,避免了 Constraints 的重复添加。

何时使用 StroyBoard,Nib,纯代码写 View

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

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

基本结论:建议纯代码开发。

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

有时候出于记录用户操作行为数据的需要,或统一配置页面的目的,会从 UIViewController 派生一个 ViewController,例天猫客户端要求所有 ViewController 都要继承 TMViewController。

未达成统一设置或执行统一逻辑的目的,使用派生手段是否有必要?

没有必要!

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

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

集成成本

对于业务层存在的所有父类来说,它们是很容易跟项目中的其他代码纠缠不清的。

业务方开发时遇到的两难问题:

  1. 要么把所有依赖全部搞定,然后基于 App 环境(如天猫)下开发 Demo
  2. 要么就是自己 Demo 写好之后,按照环境要求修改代码

上手成本

新来的工程师他不能直接按照苹果原生的做法做事情,需要额外学习成本。

架构的维护难度

减少继承,能够提高项目的可维护性。

AOP

架构师实现具体方案之前,必须要想清楚几个问题,才能决定采用哪些方案。

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

最终的方案效果:

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

不通过业务代码上对框架的主动迎合,使得业务能够被框架感知。

  1. Method Swizzling,App 启动时对 UIViewController 的方法拦截。pod’Aspects
  2. NSObject load 函数,应用启动时自动监听,这个模块只要被项目包含,就能发挥作用,不需要再项目里添加任何代码。

消除不必要的继承,拿捏尺度。

关于 MVC、MVVM 等一大堆思想

万变不离其宗,对三个角色应当如何进行数据交换。

  • 数据管理者
  • 数据展示者
  • 数据加工者

MVC

MVC (Model-View-Controller),其中 Model 就是作为数据管理者,View 作为数据展示者,Controller 作为数据加工者,Model 和 View 又是由 Controller 来根据业务需求调配,所以 Controller 还担负了一个数据流调配的功能。

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

服务端 View 和 客户端 View 真正区别:

  • 从概念上严格划分的话,服务端其实根本没有 View,拜 Http 协议所赐,我们平时所讨论的 View 只是用于描述 View 的字符串(数据),真正的 View 是浏览器。

UIView 不光可以展示 UI,还可以作为容器的一个对象。

UIViewController 中自带的 view,主要任务就是作为一个容器!

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

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

M

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

C

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

V

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

MVCS

从概念上来说,它拆分的部分是 Model,拆出来一个 Store,Store 专门负责数据存取。
实际操作角度,它拆分的是 Controller。

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

胖 Model,瘦 Model

胖 Model

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

1
2
3
4
5
6
7
8
9
10
11
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];
优点

FatModel 做了这些弱业务,Controller 就能变得 skinny,Controller 只关注强业务就行。

  1. 强业务变动可能性比弱业务大得多,弱业务相对稳定。
  2. 弱业务重复出现的频率要大于强业务,对复用性的要求更高。
缺点
  1. 较难移植,虽然只是包含了弱业务,好歹也是业务,迁移时候容易拔萝卜带出泥。
  2. MVC 架构思想更倾向于 Model 是一个 Layer,而不是一个 Object,不应该把 Layer 应该做的事情交给 Object 去做。
  3. FatModel 可能会随着软件的成长越来越 Fat,最终难以维护。

瘦 Model

瘦 Model 只负责业务数据的表达,所有业务无论强弱一律扔到 Controller,尽一切可能去编写细粒度 Model,然后配套各种 Helper 类或者方法来对弱业务做抽象,强业务依旧交给 Controller。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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) {
...
}
优点
  1. SlimModel 和业务完全无关,它的数据可以交给任何一个能处理它数据的 Helper 或其他对象,来完成业务,代码迁移时独立性强。
  2. 只是数据表达,维护成本基本为 0。
缺点
  1. Helper 这种做法也不见得好。
  2. Model 操作会出现在各种地方,违背了 DRY(Don’t Repeat Yourself) 的思路,Controller 仍然不可避免在一定程度上出现代码膨胀。

MVVM

RectiveCocoa,让 ViewModel 和 View 的信号机制在 iOS 下终于有了一个相对优雅的实现。

MVVM 着重解决的问题是尽可能减少 Controller 的任务。

MVVM 与 MVCS 共同点

  • Controller 会随着软件的成长,变很大很难维护很难测试。

不同点

  • MVCS 认为 Controller 做了一部分 Model 的事情,把它拆出来变成 Store。
  • MVVM 任务 Controller 做了太多数据加工的事情,所以把 数据加工 的任务从 Controller 中解放出来,使得 Controller 只需要专注于数据调配的工作,ViewModel 则去负责数据加工并通过通知机制让 View 响应 ViewModel 的改变。

MVVM 是基于胖 Model 的架构思路简历的,在胖 Model 中拆出两部分:Model 和 ViewModel,本质是为 Controller 减负(胖 Model 做的事情也是为 Controller 减负)。

MVVM 究竟应该如何实现

iOS 领域大部分 MVVM 架构都会使用 RectiveCocoa,但是使用 RectiveCocoa 的 iOS 应用并不一定基于 MVVM 架构。

MVVM 关键是要有 ViewModel,以及 ReactiveCocoa 带来的信号通知效果

使用 ViewModel 直接把 RawData 转变成 Controller 可以直接使用的数据。

RectiveCocoa 扮演什么角色

除 API 返回数据,View 的操作也会产生数据,主要是用户行为操作,数据从 View 走向 API 或者 Controller 的方向上,就是 RectiveCocoa 发挥的地方。

ViewModel 本质算是 Model 层,View 并不适合直接持有 ViewModel,View 产生数据 RectiveCocoa 扔信号给 ViewModel。

  1. View 并不适合直接持有 ViewModel
  2. ViewModel 可能并不是只服务于特定的一个 View,松散的绑定关系可以降低 View 和 ViewModel 之间耦合

MVVM 中,Controller 扮演什么角色

MVVM 是一定需要 Controller 参与的,弱化了 Controller 的存在感,给 Controller 做了减负。

View <-> C <-> ViewModel <-> Model,所以严格意义上来说,MVVM 是 MVCVM。

Controller 夹在 View 和 ViewModel 之间做的其中一个主要事情就是将 View 和 ViewModel 进行绑定。逻辑上,Controller 知道应当展示那个 View,应当使用哪个 ViewModel,但是 View 和 ViewModel 之间是相互不知道的,控制器就是负责它们的绑定关系。

总结:在 MVC 基础上,把 C 拆出一个 VM 专门负责数据处理的事情,就是 MVVM。

拆分心法

万变不离其宗,任何架构都是符合 MVC 的规范。拆分的方式不同,诞生了各种不同的衍生架构方案。

第一心法:保留最重要的任务,拆分其他不重要的任务

任何较大或较脏的,放在 Controller 里面的非核心逻辑,都可以考虑拆出去,架构时作为一个独立的模块去定义,以及设计实现。

第二心法:拆分后的模块要尽可能提高可服用性,尽量做到 DRY

拆出来的部分最好能够归成某一类对象,抽象出一个通用逻辑,使之复用。即使不能抽出通用逻辑,尽量抽象出一个 protocol,来实现 IOP。

第三心法:尽可能提高拆分模块后的抽象度

对于业务方来说,他只需要收集很少的信息(最小充要条件),做很少的调度(Controller 负责大模块调度,大模块里再去做小模块调度),就能够完成任务,这才是给 Controller 减负的正确姿势。

设计心法

  1. 合理拆分 MVC 来给 Controller 减负
  2. 照顾业务方的使用成本

第一心法:尽可能减少继承层级,涉及苹果原生对象的尽量不要继承

  1. 业务方做业务开发或 Demo,可以脱离 App 环境,或花更少的时间搭建环境
  2. 对业务方来说功能更加透明,符合业务方在开发时的第一直觉

第二心法:做好代码规范,规定好代码在文件中的布局,尤其是 ViewController

编码规范,分层注释。

第三心法:能不放在 Controller 做的事情就尽量不要放在 Controller 里面去做

C 承载了业务逻辑,M 和 V 模棱两可的东西也算 C,导致 C 的膨胀。

模棱两可的模块,就不要塞到 C 去了,塞到 V 或者 M 或者其他地方都比塞 C 好,便于将来拆分。

更倾向于胖 Model

业务膨胀以后,代码规模肯定少不了,既然都是要膨胀,将来拆分胖 Model 也能比拆分胖 Controller 更加容易。

第四心法:架构师是为业务工程师服务的,而不是去使唤业务工程师

态度越谦卑,就越能设计出好的架构。

小总结

View 层架构主要

  • 代码规范
  • 架构模式
  • 工具集

跨业务页面调用方案的设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
--------------             --------------             --------------
| | page call | | page call | |
| Buisness A | <---------> | Buisness B | <---------> | Buisness C |
| | | | | |
-------------- -------------- --------------
\ | /
\ | /
\ | /
\ | /
\ | /
--------------------------------
| |

| App |
| |
--------------------------------

跨业务的页面调用在多业务组成的 App 中会导致横向依赖。

后果

  1. 当一个需求需要多业务合作开发时,直接依赖,会导致某些依赖层上端的业务工程师在前期空转,依赖层下端的工程师任务繁重,而整个需求完成的速度会变慢,影响的是团队开发迭代的速度。
  2. 当开辟一个新业务时,如果已有各业务间直接依赖,新业务又依赖某个旧业务,就导致新业务的开发环境搭建困难,因为必须要把所有相关业务都塞入开发环境里,新业务才能进行开发,影响新业务迭代速度。
  3. 当一个被其他业务依赖的页面有所修改时,比如改名,设计的修改面就会特别大,影响的是造成任务量和维护成本都上升的结果。

处理

让依赖关系下沉,引入 Mediator 模式。

用中间人来召唤另一个页面,这样只要每个业务依赖这个中间人就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

-------------- -------------- --------------
| | | | | |
| Buisness A | | Buisness B | | Buisness C |
| | | | | |
-------------- -------------- --------------
\ | /
\ | /
\ | / 通过Mediater来召唤页面
\ | /
\ | /
--------------------------------
| |

| Mediater |
| |
--------------------------------
|
|

|
|

|
--------------------------------
| |

| App |
| |
--------------------------------

关于 getter 和 setter

将私有属性初始化全部放在 getter 里去做,init 和 dealloc 之外是不会出现任何类似 _property 的写法。

总结

View 层架构,主要三方面

  1. 制定良好的规范
  2. 选择好合适的架构模式(MVC,MVCS,MVVM…)
  3. 根据业务情况针对 ViewController 做好拆分,提供一些小工具方便开发

View 层架构,最好还是尽量遵循苹果已有的规范和设计思想,然后根据自己过去开发 iOS 时的经验,尽可能给业务方在开发业务时减负,提高业务代码的可维护性,就是 View 层架构的最大目标。