专注于 JetBrains IDEA 全家桶,永久激活,教程
持续更新 PyCharm,IDEA,WebStorm,PhpStorm,DataGrip,RubyMine,CLion,AppCode 永久激活教程

架构系列深入了解MVC, MVP, MVVM, VIPER架构设计模式

1、 架构设计模式简介

1、 目前主流的几种架构模式如下:

1、 架构设计模式本质目的

MVPMVVMVIPER都是从MVC演变而来,都是为了解决开发过程中的实际问题而提出来的,各有优缺点和适用的场景,本质目的都是不断的从ViewController中把逻辑拆分出去

1、 从功能来区分的话,可以从三个层面划分上面的4种架构设计模式:

  • Model层: 负责数据访问,又可以分为以下两类
    • 业务处理:日常开发中DAOService都可以算作是Model层衍生出来的业务请求模块,负责用于处理用户提交的请求。
    • 数据承载:用于专门承载业务数据的实体类,比如开发中定义的Student、User等各种Entity.
  • View层: 负责视图的展示
  • Controller/Presenter/ViewModel:Model和View之间的中介,一般负责在用户操作View时更新Model,以及当Model变化时更新View

1、 一个好的架构或者架构模式应该满足以下三点:

  • 好的职责划分
  • 可测试
  • 易用性

2、 MVC

2.1 传统的MVC

75_1.png

  • 存在的问题

1、 很难单元测试
2、 View和Model耦合严重
3、 实体之间相互耦合

2.2 苹果的MVC

苹果认为传统的MVC模式中,View通过Observer模式直接观察Model对象以获取相关的通知,而这样的设计会导致View和Model对象不能被广泛复用,因为View与其观察的Model之间存在耦合关系。因此,苹果版MVC与传统MVC基本一致,只是隔离了View和Model。

75_2.png在iOS中,UIViewController和UIView是一一对应的。MVC最终一点点变成了 Massive-View-Controller(胖VC)。最终的实际情况如下 75_3.png随着业务的深入,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 75_4.png

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

75_5.png含义解释:

  • Model:提供数据模型
  • View:负责视图展示
  • ViewModel: 用于描述View的状态,例如View的颜色、显示的文字等属性类的信息,将View抽象成了一个特殊的模型,并且持有和管理Model,维护业务逻辑

4.1 特点

  • MVVM相比较于MVP,将Presenter变成ViewModelViewModel可以理解成是View的数据模型和Presenter的合体(多理解这句话,精华)
  • MVVM中的数据可以实现双向绑定,即View层数据变化则ViewModel中的数据也随之变化,反之ViewModel中的数据变化,则View层数据也随之变化
  • MVVM模式和MVC模式类似,主要目的是分离视图(View)和模型(Model)

4.2 数据的双向绑定

在MVP中,View通过接口的方式来描述自己,在MVVM中,则通过ViewModel来描述自己的特征。那么ViewModel如何将自己的变化更新到View上呢?所以MVVM经常和数据绑定一起出现,可以使用 KVO 或 Notification 技术达到这种效果。

如果我们自己不想自己实现,那么我们有两种选择:

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

75_6.png

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

参考文章

75_7.png

文章永久链接:https://tech.souyunku.com/31627

未经允许不得转载:搜云库技术团队 » 架构系列深入了解MVC, MVP, MVVM, VIPER架构设计模式

JetBrains 全家桶,激活、破解、教程

提供 JetBrains 全家桶激活码、注册码、破解补丁下载及详细激活教程,支持 IntelliJ IDEA、PyCharm、WebStorm 等工具的永久激活。无论是破解教程,还是最新激活码,均可免费获得,帮助开发者解决常见激活问题,确保轻松破解并快速使用 JetBrains 软件。获取免费的破解补丁和激活码,快速解决激活难题,全面覆盖 2024/2025 版本!

联系我们联系我们