摘要
这篇文档主要讲解MVVM架构的具体设计,分别有层级划分、层级职责和层级之间的通信,层级内的模块划分、模块职责和模块之间的通信,模块内使用的设计模式和设计模式的使用场景。作为程序设计的准则和规范,并为以后的框架改进提供参考。
设计目标
架构设计按优先级需要达成以下目标:
- 层级、模块之间的低耦合性
- 逻辑层的复用性
- 逻辑层的高度灵活性,可快速应对需求变化,达到可用逻辑模块的拼装完成一个功能的情形
- 单元测试可覆盖到业务逻辑层
- 各模块实现足够简单,新手可以快速上手
层级划分
按照MVVM得设计思想,总体上分为三大块,分别是View层、ViewModel层、Model层。
- View层负责视图逻辑。
- Model层负责处理数据逻辑。
- ViewModel层负责处理业务逻辑,为View层提供数据源。
从具体实现上来说,这样的划分粒度太粗,View层和ViewModel层都需要细分。具体划分如下:
- View层:系统控件扩展层,自定义控件层,视图逻辑实现层,ViewController层。
- ViewModel层:页面业务逻辑实现层。
- Model层:数据逻辑层。
整体结构大体如下:
View层
这一层主要功能是展示,是用户最直接接触到得层级,也是需求变动最频繁,代码改动量最大的层级。这一层尽量以小而简、多模块聚合的方式设计。
View层只能用来实现视图逻辑,对业务逻辑了解视图需求,但不了解实现细节。
UI工具:
这一层主要给页面绘制、自定义控件等提供方便的工具,例如Theme。
系统控件扩展 :
这一层主要是对系统提供的控件做一些轻微的扩展。控件扩展力求简短,不要过于复杂,过于复杂的东西应该做成自定义控件。
自定义控件:
对于系统控件无法满足的设计要求,通常为其创建自定义控件。
设计规范:
禁止涉及任何业务逻辑和业务数据。
尽量不要暴露内部实现和内部控件,继承于系统控件的自定义控件中父类已经暴露的除外
尽量以属性的方式提高控件的可配置性。
设计时尽量考虑全面,而不是仅仅实现当下的需求。
事件的绑定,必须统一提供RACCommand类型的属性,依赖外部设定RACCommand对象。
众多属性的设置方法,禁止依赖调用次数和调用顺序。
视图逻辑实现
视图逻辑实现、动画、页面跳转、弹框、提示等操作必须放到ViewController处理。
设计规范:
- ViewController禁止处理业务逻辑。
- 每个ViewController只能最多持有1个ViewModel。
- ViewController禁止持有业务数据,全部依赖于ViewModel。
- View的初始化必须放到重写的loadView方法中,子类必须在最开始显式调用父类的loadView方法。
- 绑定View与ViewModel必须放到ViewController的重写的viewDidLoad方法中,子类必须在最开始显式调用父类的viewDidLoad方法。
- 较大的视图逻辑,建议通过多个可复用的子ViewController来组织。
- 子ViewController的ViewModel可以自己生成或者依赖主外部注入,例如主ViewController传递。
- 子ViewController的数据绑定必须依赖于自己的ViewModel,禁止向上遍历找到主的ViewModel来绑定。
关于子ViewController的ViewModel使用情况,见下图
ViewModel层
这一层是主要负责程序的业务逻辑处理,必须通过Model层来处理数据的增删改查,转换后只能暴露给View层基本类型的数据。
与View层的关系
设计规范:
- 只能对View提供基本类型的属性(数值型、Boolean、NSString、NSDate)或者包含这些作为sendNext值的RACSignal。
- 等待调用的功能必须封装成RACCommand。
- 暴露的RACCommand必须可以通过executing信号来判断是否执行。
- 每个RACCommand的错误必须通过errors信号来通知给View层。
- 通知给View层的事件必须封装成RACSignal,其sendNext值可以为基本类型组成的元组(RACTuple)或者nil。
- 所有的setter方法,禁止带有副作用,副作用利用Observer来实现。
- 集合数据源必须由SAKFetchedResultsController提供。
下面详细讲解View层和ViewModel层的设计规范
ViewModel层
这一层是主要负责业务逻辑处理,并给View层提供数据源。
设计规范
VM的设计粒度应当越小越好
根据功能划分可以包含子VM
子VM之间的关系处理,数据同步和数据共享由主VM处理
独立的VM之间相互不可见,(VM之间不可直接调用,子VM除外,兄弟VM也不可见)
全局数据的读写通过统一的接口进行
全局数据有读写权限,自身功能不相干的全局数据仅有读取权限
不涉及UI操作的业务逻辑处理和判断全部放到VM层,如果处理对象完全是自身的属性和数据可以放在Model层处理
命名规范
command命名体现用户意图, 以Command为后缀。例: loadDealListCommand
事件以Block形式通知UI,Block统一命名形式,命名反映事件的内容,命名以Block为后缀。例:void (^areaCountChangedBlock)();
一些常见的command和函数使用统一命名规范
加载数据使用load前缀,例:loadDealListCommand,
刷新使用refresh前缀,例:refreshDealListCommand
编写规范
VM头文件
由VM提供给VC或View当做数据源的内容全部以属性的形式提供
数组类的数据源使用FetchedResultsController
对外提供的操作使用用Command封装
VM应保证Command执行状态正确,不论Command是同步还是异步
对外不直接提供函数调用
存在VM主动的单向的对外通知的场景,又不依赖某个属性值的场景(事件),使用Block
VM 内部编写建议(.m文件,由上门项目做示例)
所有VM都继承于OSSViewModel
带有列表类的操作可以继承于OSSListViewModel
Command封装的是同步操作可以用commandWithBlock初始化
Command封装的是异步操作可以用commandWithAsyncBlock初始化
具体的业务逻辑应当封装在函数内操作,这样可以给多个Command复用
异步函数都带有commandFinish参数,此参数可以保证正确的command执行状态
异步操作使用singal封装
VM内部的逻辑错误和Model返回的逻辑错误统一由Command.error处理外部
内部的逻辑调用,如果有command封装应该调用command来执行,这样可以保持逻辑和状态的统一
自身属性调用,没有特殊需求的时候使用self.语法。
vm中的属性设置有多个使用场景应该在.h文件分开,并且注明。例如,某个属性在设置的时候会引起别的属性和逻辑联动,应该在.h中提供setXXXCommand,将这种场景分离出来。之所以这样做是因为无法保证在任何情况下设置这个属性都需要引起逻辑联动,比如VM内部的属性设置经常不需要引起逻辑联动。并且你无法知道这个属性和联动的属性被多少东西绑定,这将引起逻辑上得混乱甚至导致流程失控。
@interface OSSProductListFilterViewModel : OSSViewModel
@property (nonatomic, strong) RACCommand *changeColorCommand;
@property (nonatomic, strong) RACCommand *changeStyleCommand;
@property (nonatomic, strong) RACCommand *changeSortCommand;
@property (nonatomic, strong) OSSProductFilterInfo *currentColor;
@property (nonatomic, strong) OSSProductFilterInfo *currentStyle;
@property (nonatomic, strong) OSSProductFilterInfo *currentSort;
@property (nonatomic, strong) OSSFetchedResultsController *colorFilterList;
@property (nonatomic, strong) OSSFetchedResultsController *styleFilterList;
@property (nonatomic, strong) OSSFetchedResultsController *sortFilterList;
@property (nonatomic, copy) void (^filtersChangedBlock)(OSSProductListFilterViewModel *sender, id filter);
@end
View层
设计规范
VC不直接处理任何业务逻辑
一个VC与一个ViewModel一一对应
交互操作使用command绑定
数据关联使用绑定、RAC方式处理
VC可以包含多个子VC
子VC的数据绑定放到子VC处理
子VC的ViewModel可由主VC传递进去也可以由子VC自己生成
编写规范:
页面跳转、弹框、提示等 操作放到VC处理
View初始化放ViewLoad处理
与ViewModel的数据绑定操作,放到ViewDidLoad中,临时生成控件的绑定可以放到控件初始化后做
VC中不保存业务数据,业务数据全部放到VM中
自定义控件,用户的交互需要外部处理,使用Command
自定义控件尽量少的暴露内部控件
自定义控件暴露的属性尽量少的涉及到业务数据
自定义控件任何属性的Set方法,不要预设使用次数和顺序依赖,在任何时候被调用都不应该产生副作用
VC、VM组织形式
当VC中存在多个子VC,VM中也存在多个子VM时,VC和VM的组织形式有两种,
当VM与VM之间存在关联关系的时候,用主VM将这两者封装起来,结构如下:
若VM与VM之间没有关系,那么子VC可以自己去关联对应的VM,如下图所示: