1、 架构设计模式简介
1、 目前主流的几种架构模式如下:
1、 架构设计模式本质目的
MVP
、MVVM
、VIPER
都是从MVC
演变而来,都是为了解决开发过程中的实际问题而提出来的,各有优缺点和适用的场景,本质目的都是不断的从ViewController中把逻辑拆分出去
1、 从功能来区分的话,可以从三个层面划分上面的4种架构设计模式:
Model
层: 负责数据访问,又可以分为以下两类- 业务处理:日常开发中
DAO
、Service
都可以算作是Model层衍生出来的业务请求模块,负责用于处理用户提交的请求。 - 数据承载:用于专门承载业务数据的实体类,比如开发中定义的Student、User等各种Entity.
- 业务处理:日常开发中
View
层: 负责视图的展示Controller/Presenter/ViewModel
:Model和View之间的中介,一般负责在用户操作View时更新Model,以及当Model变化时更新View
1、 一个好的架构或者架构模式应该满足以下三点:
- 好的职责划分
- 可测试
- 易用性
2、 MVC
2.1 传统的MVC
- 存在的问题:
1、 很难单元测试
2、 View和Model耦合严重
3、 实体之间相互耦合
2.2 苹果的MVC
苹果认为传统的MVC模式中,View通过Observer模式直接观察Model对象以获取相关的通知,而这样的设计会导致View和Model对象不能被广泛复用,因为View与其观察的Model之间存在耦合关系。因此,苹果版MVC与传统MVC基本一致,只是隔离了View和Model。
在iOS中,UIViewController和UIView是一一对应的。MVC最终一点点变成了
Massive-View-Controller
(胖VC)。最终的实际情况如下 随着业务的深入,ViewController中的代码越来越多, 分派和取消网络请求,业务逻辑处理、delegate和datasource的处理等都被放在了这里,变得难以维护和测试。
2.3 项目中怎么用
- 不太规范的写法
苹果的MVC准则Model和View是解耦的,但是实际很多项目中都是违背了这个准则的
var userCell = tableView.dequeueReusableCellWithIdentifier("identifier") as UserCell
userCell.configureWithUser(userModel) //Cell(一个View)跟一个Model直接绑定了
Cell跟一个Model直接绑定了,但是这种情况总是发生,这样使用并不会有什么问题,但不易与单元测试。
如果你严格遵循MVC,那么你应该从Controller配置cell(直接设置cell的属性赋值),而不是将Model传递到cell中,但这将增大Controller的代码量。
- 个人建议的写法
定义协议,model继承协议
protocol UserDataProtocal {
var name: String {get}
var age: Int {get}
}
struct User: UserDataProtocal {
var name: String
var age: Int
}
var data = userModel as? UserDataProtocal
var userCell = tableView.dequeueReusableCellWithIdentifier("identifier") as UserCell
userCell.configureWithUser(data)//传的是数据协议
2.4 总结
苹果MVC缺点:
- View和Model实现了分离,但是View与Controller仍是紧耦合。
- 你只能测试你的Model。
- Contoller处理的事情过多,职责划分不够细
- 不适合小型、中等规模的应用程序
苹果MVC优点:
- 耦合性低
- 重用性高
- 生命周期成本低
- MVC使开发和维护用户接口的技术含量降低
- 可维护性高
- 部署快
苹果的MVC,实际上是一个视图驱动的设计,Controller只是为了管理View而存在的。苹果把UIViewController和Model的关系设计交给了我们自己。
3、 MVP
MVP是从MVC模式演变而来的,MVC中的Controller换成了Presenter,目的就是为了完全切断View跟Model之间的联系,由Presenter充当桥梁,
- View负责界面展示和布局管理,向Presenter暴露视图更新和数据获取的接口
- Presenter负责接收来自View的事件,通过View提供的接口更新视图,并管理Model
3.1 特点
- View 与 Model 不通信,都通过 Presenter 传递,Presenter完全把Model和View进行了分离,主要的程序逻辑在Presenter里实现
- View 非常薄,不部署任何业务逻辑,称为”被动视图”(Passive View),即没有任何主动性,而 Presenter非常厚,所有逻辑都部署在那里
- Presenter与具体的View是没有直接关联的,而是通过定义好的接口进行交互,从而使得在变更View时候可以保持Presenter的不变,这样就可以重用。
- Model中会负责网络数据的存取
3.2 demo
考虑到篇幅问题:这里只给出一个简单的缩写版
- controller
class ViewController: LoginViewProtocol {
let presenter = LoginPresenter.init(view: self)
}
- Presenter
class LoginPresenter {
var model: LoginModelProtocol?
weak var view: LoginViewProtocol?
init(view: LoginViewProtocol?) {
self.view = view
model = LoginModel(present: self)
}
- model
class LoginModel {
weak var present: LoginPresenter?
init(present: LoginPresenter?) {
self.present = present
}
func login(usrName:String,pwd:String,callback:((String)->Void)) {
//发起网络请求 处理方法要封装,不能耦合
print("进入model")
HttpUtils.post(usrName: usrName, pwd: pwd) { (result) in
self.present.dosomething()
}
}
3.3 MVP的优点
- 模型与视图完全分离,我们可以修改视图而不影响模型
- 可以更高效地使用模型,因为所有的交互都发生在一个地方——Presenter内部 我们可以将一个Presenter用于多个视图,而不需要改变Presenter的逻辑,这个特性非常的有用,因为视图的变化总是比模型的变化频繁
- 把逻辑放在Presenter中,进行单元测试,脱离用户接口来测试这些逻辑
3.4 MVP的缺点
- 接口爆炸
- Presenter很重
3.5 MVC和MVP之间的区别
- 在传统MVC中,View与Model层直接交互,读取数据,不通过Controller
- 在MVP中,View与Model层不直接交互,而是通过Presenter与Controller进行交互,所有的交互都是发生在Presenter 的内部
4、MVVM
MVVM最主要的就是ViewModel
含义解释:
Model
:提供数据模型View
:负责视图展示ViewModel
: 用于描述View的状态,例如View的颜色、显示的文字等属性类的信息,将View抽象成了一个特殊的模型,并且持有和管理Model,维护业务逻辑
4.1 特点
MVVM
相比较于MVP
,将Presenter
变成ViewModel
,ViewModel
可以理解成是View的数据模型和Presenter的合体(多理解这句话,精华)- MVVM中的数据可以实现双向绑定,即View层数据变化则ViewModel中的数据也随之变化,反之ViewModel中的数据变化,则View层数据也随之变化
- MVVM模式和MVC模式类似,主要目的是分离视图(View)和模型(Model)
4.2 数据的双向绑定
在MVP中,View通过接口的方式来描述自己,在MVVM中,则通过ViewModel来描述自己的特征。那么ViewModel如何将自己的变化更新到View上呢?所以MVVM经常和数据绑定一起出现,可以使用 KVO 或 Notification 技术达到这种效果。
如果我们自己不想自己实现,那么我们有两种选择:
- 基于KVO进行绑定,如RZDataBinding 和SwiftBond
- 完全的函数响应式编程,例如ReactiveCocoa、RxSwift
4.3 MVVM的优点
- 独立开发: 开发人员可以专注于业务逻辑和数据的开发(ViewModel),设计人员可以专注于页面设计,使用Expression Blend可以很容易设计界面并生成xml代码
- 可测试: 界面向来是比较难于测试的,而现在测试可以针对ViewModel来写
- 可重用性,可以把一些视图逻辑放在一个ViewModel里面,让很多view重用这段视图逻辑
4.4 MVVM缺点
- 数据绑定使得 Bug 很难被调试。
- 对于过大的项目,数据绑定需要花费更多的内存
4.4 demo
这里由于篇幅原因,不写太细,有个大概的认识就行实现方案有很多,可自行了解哪种适合自己
- ViewModel的定义
class RegisterViewModel {
// 刷新控件的信号
private let refreshSubject = PublishSubject<Void>()
// 供外部订阅
let result: Driver<RegisterStatsModel>
// loading
let loading: Driver<Bool>
// error
let error: Driver<Error>
init(dateTypeAndDate: Driver<(DateType, Date)>) {
loading = loadingTracker.asDriver()
error = errorTracker.asDriver()
result = Driver.combineLatest(refreshSubject.asDriverOnErrorJustComplete(), dateTypeAndDate)
.flatMapLatest{ (_, arg) -> Driver<RegisterStatsModel> in
let (dateType, date) = arg
return RegisterService.getRegisterStatis(date: date, type: dateType)
}
}
}
- ViewController中的使用
func bindViewModel() {
viewModel
.result
.drive(onNext: { [unowned self] model in
//dosomething
}
})
.disposed(by: rx.disposeBag)
// loading
viewModel.loading
.drive(view.rx.toastActivity)
.disposed(by: rx.disposeBag)
// error
viewModel.error
.drive(view.rx.toast)
.disposed(by: rx.disposeBag)
}
实际使用过程中,我发现很多项目View中传入的还是model,不是ViewModel,其实这是违背了MVVM的原则,这一点需要注意
4.5 ViewModel引出的问题
把业务逻辑放到ViewModel中,虽然能够为UIViewController减负,但是只是把问题转移了,最终ViewModel还是会变成另一个Massive-ViewModel
。
和MVP相比,MVVM用了一种更优雅的方式来抽象View。但它和MVP其实是类似的,只做了View和Model的解耦,仍然没有对Controller进行进一步的细分。
那么如何对Controller进行进一步的职责细分呢?答案就是VIPER
。
5、 VIPER
VIPER
架构是根据由外向内的依赖关系来设计的,从外向内表现为:View
-> Presenter
-> Interactor
-> Entity
->Router
5.1 职责划分
View
- 提供完整的视图,负责视图的组合、布局、更新
- 向Presenter提供更新视图的接口
- 将View相关的事件发送给Presenter
Presenter
- 接收并处理来自View的事件
- 向Interactor请求调用业务逻辑
- 向Interactor提供View中的数据
- 接收并处理来自Interactor的数据回调事件
- 通知View进行更新操作
- 通过Router跳转到其他View
Router
- 提供View之间的跳转功能,减少了模块间的耦合
- 初始化VIPER的各个模块
Interactor
- 维护主要的业务逻辑功能,向Presenter提供现有的业务用例
- 维护、获取、更新Entity
- 当有业务相关的事件发生时,处理事件,并通知Presenter
Entity
- 普通实体
5.2 优点
- 可测试性好。UI测试和业务逻辑测试可以各自单独进行。
- 易于迭代。各部分遵循单一职责,可以很明确地知道新的代码应该放在哪里。
- 隔离程度高,耦合程度低。一个模块的代码不容易影响到另一个模块。
- 易于团队合作。各部分分工明确,团队合作时易于统一代码风格,可以快速接手别人的代码。
5.3 缺点
- 一个模块内的类数量增大,代码量增大,在层与层之间需要花更多时间设计接口。
- 模块的初始化较为复杂,打开一个新的界面需要生成View、Presenter、Interactor,并且设置互相之间的依赖关系。而iOS中缺少这种设置复杂初始化的原生方式
5.4 demo
由于代码过多,完整的实现放到Github了 VIPER代码传送门
查看部分demo代码
protocol ReposViewType: class {
var presenter: ReposPresenterType? { get set }
func didReceiveRepos()
func showLoading()
func hideLoading()
func displayAlert(for id: Int)
}
protocol ReposWireframeType: class {
static func createReposModule() -> UIViewController
}
protocol ReposPresenterType: class {
var view: ReposViewType? { get set }
var interactor: ReposInteractorInputsType? { get set }
var wireframe: ReposWireframeType? { get set }
func onViewDidLoad()
func didChangeQuery(_ query: String?)
func didSelectRow(_ indexPath: IndexPath)
func numberOfListItems() -> Int
func listItem(at index: Int) -> RepoViewModel
}
protocol ReposInteractorInputsType: class {
var presenter: ReposInteractorOutputsType? { get set }
func fetchRepos(for query: String)
func fetchInitialRepos()
}
protocol ReposInteractorOutputsType: class {
func didRetrieveRepos(_ repos: [Repo])
}
protocol ReposRemoteDataManagerType: class {
func fetchRepos(for query: String, completion: @escaping ([Repo]) -> ())
}
5.5 总结
VIPER是单个界面模块内的架构设计,并不是整个app架构层面的设计,和app的整体架构没有多大的关系,也不存在过早使用VIPER的情况。所以,严格来说,是复杂界面更适合VIPER,而不是大型app更适合VIPER
参考文章