iOS架构设计由浅入深
在iOS开发过程中,我们通常会根据业务对工程架构进行设计,以达到对业务的完美契合,接下来我们会由浅入深的对架构设计进行系统的讨论
什么样的架构才是好的架构?
如何去设计一个符合项目要求的“好”架构?我想主要有两点去考虑 每个类应该详尽的,并且具备清晰的架构角色。这样易于代码理解,便于我们在想修改代码的时候进行修改,一眼就可以看出这个对象是否是单独角色还是复杂逻辑角色,以至于我们在写代码的时候不会违背这样的原则。
经过前辈们总结,目前对于iOS开发,主要有MVC、MVVM、Viper等架构模式。一起看看吧
MVC:
Model-View-Controller,简称MVC MVC架构模式将一个工程分解为三层:
- Model
- View
- Controller
Model(模型层)
Model层负责处理应用中的业务逻辑。他管理着应用状态。既包含读写数据,维持应用状态,也包含与数据管理相关的任务,比如网络请求和数据校对。
View(视图层)
视图层包含两个主要任务:
- 将数据呈现给用户
- 处理用户交互
MVC架构模式最核心的原则是:View层不需要关心Model层,View层是”哑巴“对象,View层只需要做一件事,就是将数据呈现给用户。并且不需要知道或者了解这些数据是怎么来的。这样一来,就会使得View层更加灵活且易于重用。
Controller(控制器层)
View层和Model层是通过一个或多个Controller进行胶合的。在一个iOS应用中,这个胶合剂就是ViewController,一个UIViewController及其子类的实例。在MacOS中,这个胶合剂就是NSWindowController及其子类的实例。
Controller层需要知道View层和Model层。这通常会导致高度耦合,使得Controller成为MVC模式下最小的可重用应用组件。View层和Model层不需要知道Controller层,Controller层持有View层和Model层并使两者在Controller中进行互动
MVC的优势:
- 关注点分离
MVC的优势是清晰的关切点分离,每一层明确的负责不同方面的内容。在大多数APP中,哪些属于View层,哪些属于View层,哪些属于Model层,其实并没有明确的界限。 进入控制器的内容通常并不明确。其结果是,Controller层经常被用于不明确属于View层和Model层的内容
- 重用性
Controllers通常并不是可重用的,但View和Model对象几乎很容易进行重用。如果MVC正确执行的话,View层和Model层应该是最镇定的可重用组件了
- 不足
如果你花时间去阅读了iOS/MacOS开发的书籍或教程的话,你可能会搞混大家对于MVC的解释,为啥呢?MVC怎么了? 清晰的关注点分离其实是非常好的,这一点易于你的开发者生涯。也会使得你的工程更容易的进行架构设计。但这只是好的一面。很多代码不属于View层和Model层。我们通常会将其丢进Controller层,这样也能解决问题,不是吗?真的是这样吗?我们来举个例子看看
- 范例
数据格式化是一个常见的任务。想象一下,你正在开发一款发票应用,每个发票都有一个创建日期。依赖于用户的区域设置,发票日期需要进行不同的格式化
发票的创建日期保存在Model层,View层展示的是格式化之后的日期。但是谁来负责格式化日期呢?Model?View?回想一下,View层和Model层不能直接交互,View层只负责展示数据给用户,但是为什么Model层就应该负责用户交互相关的任务呢? 稍等,Controller怎么样呢?把这段业务丢进Controller里,那得几千行代码啊,最后你会得到一个臃肿的Controller,最后测试的时候有你头疼的。
- 总结
- MVC能很好的将我们所需要设计的app进行分层处理,对于View和Model有着很好的重用效果,从而简化我们对部分复杂业务的处理
- MVC更多的可以定性为一种轻量级架构,适合业务量不大的项目
- 随着业务量的增多,MVC的架构力量就显得有点匮乏了,因为会有很多很多的业务代码堆积到Controller层,这样就导致我们的ViewController代码量巨大(我见过最多的有2800行,头皮发麻)不利于日后的代码阅读和维护。
MVVM:
Model-View-ViewModel,简称MVVM,根据该架构名称得知,MVVM包含以下四层:
- Model
- View
- ViewModel
- Controller
ViewModel的实现通常是非常直接的,它所能做的就是将数据从Model转译成View层可以展示的Value值。Controller不再关注那些非必要任务,因为ViewModels跟他们所消耗的Models有着非常紧密的联系,ViewModel通常会更多的考虑Model而非View 注:这里所说的ViewModel更倾向于业务层代码,当然还有很多非业务层代码我们需要处理,这时可能就会将这些非业务代码放在Controller里了
MVVM模式是如何工作的
下面这张图,需要我们牢牢记在脑海中,这张图十分重要,关系到我们对MVVM的代码执行顺序的理解
MVVM的基本规则
- View不关注持有它的VC里面的具体实现,和MVC一样View只负责展示数据给用户
- VC不关注Model的具体实现,这是MVC和MVVM的本质区别
- Model不关注持有它的VM的具体实现,这是跟MVC的另一区别
- VM持有Model,但Model不持有VM
- VC持有View,但View不持有VC
- VC持有ViewModel,VC通过一个或多个ViewModel来交互处理Model
明白MVVM的原理即可,这里不做示例演示,有兴趣的话可以去github搜一搜相关的架构代码。
VIPER
ViewView-Interactor-Presenter-Entity-Router,简称VIPER。
V: ViewView,组织对内部的布局,对外暴露用户更新UI的接口,自己不主动更新UI I: Interactor,实现和封装各种业务的UseCase,可以获取各种Manager或者Service用于实现业务逻辑 P: Presenter,调用Interactor提供的UseCase执行业务逻辑。Presenter作为业务和View的中转站,不高喊业务实现代码。而是调用各种UseCase E: Entity,这里是业务的Model层,是业务逻辑的实体 R: Router,路由工具,用于页面之间的跳转,实现页面之间的解耦
其中: Router是入口,View/VC是由路由器创建的,其他组件也是由路由器中创建并组装成Viper链的 Persenter是中枢神经,因为整个Viper链都要通过他来串联起来,比如V和Interactor都会和他进行双向绑定,即互相引用。
较大项目中常见问题
- MVC设计模式导致C层代码臃肿,UI代码以及业务逻辑混杂在一起
- APPDelegate启动代码较为凌乱,各种事件没有归类
- 各种页面跳转之间存在耦合,需要在各个页面引入需要跳转的页面
随着项目越来越庞大,凸显的问题也会越来越多。随着项目的更新迭代,会出现隐藏的bug,比如模块A,模块B,模块C,更多页面的首页Controller的代码越来越多,越来越难以维护。MVC的设计模式的缺点也渐渐暴露出来。一个页面中会有三千多行的代码,迭代也会出现问题。
项目中曾经尝试过MVVM设计模式,也只是分离出了view的代码。而且ViewModel的职能并不明显。
VIPER设计模式在客户端中的应用
VIPER设计模式路由详细描述:
整个项目设置一个总路由工厂类TMRoute,我们根据业务分成四大模块:TMARoute、TMBRoute、TMCRoute、TMDRoute
各个大模块统筹自己内部的小模块。每一个ViewController实现一个Protocol,而在相应大模块中维护所有的protocol。
项目中的其他角色设计:
模块中的基础类设计
模块中的基础协议
模块中的基础类和协议详细设计
class TMBasePresenter {
var dataDic: Dictionary<String:Any> = [:]
func init(with dataSource: TMBaseDataSource, and EventHandler: TMBaseEventHandler) {
dataSource.superPresenter = self
eventHandler.superPresenter = self
}
}
protocol TMBaseInteractorProtocol {
weak var superPresenter: TMBasePresenter?
}
protocol TMBaseDataSourceProtocol {
}
protocol TMBaseEventHandlerProtocol {
}
class TMBaseInteractorProtocol {
}
使用protocol可以在编译阶段就可以查找出问题。而且protocol的不仅仅有View可以实现,Service和Manager类都可以实现。路由的方式可以解决Controller之间的耦合。也可以配合H5端做一个URLSchem跳转原生页面,只需要在字符串和Protocol之间做一个adapter。
protocol TMMineProtocol {
}
protocol TMMineDataSourceProtocol {
func numberOfSectionInTableView(tableview: UITableView) -> Int
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int
...
}
protocol TMMineEventHandlerProtocol {
tableview(tableView: UITableView, didSelectedRowAtIndexPath indexPath: IndexPath)
settingBtnClicked(btn: UIButton)
...
}