框架篇—MVC、MVP、MVCS、MVVM、VIPER使用关系总结

96
LeiLv
0.3 2017.08.19 21:30* 字数 18345

MVC MVC全名是Model View Controller,是模型(model)-视图(view)-控制器(controller)的缩写,一种软件设计典范,用一种业务逻辑、数据、界面显示分离的方法组织代码,将业务逻辑聚集到一个部件里面,在改进和个性化定制界面及用户交互的同时,不需要重新编写业务逻辑。

MVC

MVC全名是Model View Controller,是模型(model)-视图(view)-控制器(controller)的缩写,一种软件设计典范,用一种业务逻辑、数据、界面显示分离的方法组织代码,将业务逻辑聚集到一个部件里面,在改进和个性化定制界面及用户交互的同时,不需要重新编写业务逻辑。MVC被独特的发展起来用于映射传统的输入、处理和输出功能在一个逻辑的图形化用户界面的结构中。

数据关系

View 接受用户交互请求

View 将请求转交给Controller

Controller 操作Model进行数据更新

数据更新之后,Model通知View更新数据变化

View 更新变化数据

方式

所有方式都是单向通信

结构实现

View :使用 Composite模式

View和Controller:使用 Strategy模式

Model和 View:使用 Observer模式同步信息

使用

MVC中的View是可以直接访问Model的!从而,View里会包含Model信息,不可避免的还要包括一些业务逻辑。在MVC模型里,更关注的Model的不变,而同时有多个对Model的不同显示,及View。所以,在MVC模型里,Model不依赖于View,但是 View是依赖于Model的。不仅如此,因为有一些业务逻辑在View里实现了,导致要更改View也是比较困难的,至少那些业务逻辑是无法重用的。

MVP

mvp的全称为Model-View-Presenter,Model提供数据,View负责显示,Controller/Presenter负责逻辑的处理。MVP与MVC有着一个重大的区别:在MVP中View并不直接使用Model,它们之间的通信是通过Presenter (MVC中的Controller)来进行的,所有的交互都发生在Presenter内部,而在MVC中View会直接从Model中读取数据而不是通过 Controller。

数据关系

View 接收用户交互请求

View 将请求转交给 Presenter

Presenter 操作Model进行数据更新

Model 通知Presenter数据发生变化

Presenter 更新View数据

MVP的优势

Model与View完全分离,修改互不影响

更高效地使用,因为所有的逻辑交互都发生在一个地方—Presenter内部

一个Preseter可用于多个View,而不需要改变Presenter的逻辑(因为View的变化总是比Model的变化频繁)。

更便于测试。把逻辑放在Presenter中,就可以脱离用户接口来测试逻辑(单元测试)

方式

各部分之间都是双向通信

结构实现

View :使用 Composite模式

View和Presenter:使用 Mediator模式

Model和Presenter:使用 Command模式同步信息

MVC和MVP区别

MVP与MVC最大的一个区别就是:Model与View层之间倒底该不该通信(甚至双向通信)

MVC和MVP关系

MVP:是MVC模式的变种。

项目开发中,UI是容易变化的,且是多样的,一样的数据会有N种显示方式;业务逻辑也是比较容易变化的。为了使得应用具有较大的弹性,我们期望将UI、逻辑(UI的逻辑和业务逻辑)和数据隔离开来,而MVP是一个很好的选择。

Presenter代替了Controller,它比Controller担当更多的任务,也更加复杂。Presenter处理事件,执行相应的逻辑,这些逻辑映射到Model操作Model。那些处理UI如何工作的代码基本上都位于Presenter。

MVC中的Model和View使用Observer模式进行沟通;MPV中的Presenter和View则使用Mediator模式进行通信;Presenter操作Model则使用Command模式来进行。基本设计和MVC相同:Model存储数据,View对Model的表现,Presenter协调两者之间的通信。在 MVP 中 View 接收到事件,然后会将它们传递到 Presenter, 如何具体处理这些事件,将由Presenter来完成。

如果要实现的UI比较复杂,而且相关的显示逻辑还跟Model有关系,就可以在View和 Presenter之间放置一个Adapter。由这个 Adapter来访问Model和View,避免两者之间的关联。而同时,因为Adapter实现了View的接口,从而可以保证与Presenter之 间接口的不变。这样就可以保证View和Presenter之间接口的简洁,又不失去UI的灵活性。

使用

MVP的实现会根据View的实现而有一些不同,一部分倾向于在View中放置简单的逻辑,在Presenter放置复杂的逻辑;另一部分倾向于在presenter中放置全部的逻辑。这两种分别被称为:Passive View和Superivising Controller。

MVVM

MVVM是Model-View-ViewModel的简写。微软的WPF带来了新的技术体验,如Silverlight、音频、视频、3D、动画……,这导致了软件UI层更加细节化、可定制化。同时,在技术层面,WPF也带来了 诸如Binding、Dependency Property、Routed Events、Command、DataTemplate、ControlTemplate等新特性。MVVM(Model-View-ViewModel)框架的由来便是MVP(Model-View-Presenter)模式与WPF结合的应用方式时发展演变过来的一种新型架构框架。它立足于原有MVP框架并且把WPF的新特性糅合进去,以应对客户日益复杂的需求变化。

数据关系

View 接收用户交互请求

View 将请求转交给ViewModel

ViewModel 操作Model数据更新

Model 更新完数据,通知ViewModel数据发生变化

ViewModel 更新View数据

方式

双向绑定。View/Model的变动,自动反映在 ViewModel,反之亦然。

使用

可以兼容你当下使用的 MVC/MVP 框架。

增加你的应用的可测试性。

配合一个绑定机制效果最好。

MVVM优点

MVVM模式和MVC模式一样,主要目的是分离视图(View)和模型(Model),有几大优点:

1. 低耦合。View可以独立于Model变化和修改,一个ViewModel可以绑定到不同的”View”上,当View变化的时候Model可以不变,当Model变化的时候View也可以不变。

2. 可重用性。你可以把一些视图逻辑放在一个ViewModel里面,让很多view重用这段视图逻辑。

3. 独立开发。开发人员可以专注于业务逻辑和数据的开发(ViewModel),设计人员可以专注于页面设计,生成xml代码。

4. 可测试。界面素来是比较难于测试的,而现在测试可以针对ViewModel来写。

mvc,mvp,mvvm三者演化

说明

任何的项目框架,都是为项目服务的。没有绝对的好坏之分,只有更合适的选择。在项目进展的不同阶段,做出最合适的调整,才是是更适合团队项目发展的框架。项目设计者要谨记,任何的项目设计,都是要围绕项目发展阶段,团队成员规模,和团队整体能力而定的。切莫为了设计而设计,为了框架而框架。快速,高效的配合整个团队进展项目,才是最合适的架构。才是一个程序员为成一个leader,成为一个架构师的必经之路。

对于设计模式的学习是一件容易上瘾的事情,所以先提醒你一下:在你读完这篇文章之后,可能会比读之前有更多的疑问,比如:

(MVC)谁来负责网络请求:是 Model 还是 Controller?

(MVVM)我该怎么去把一个 Model 传递给一个新创建的 View 的 ViewModel?

(VIPER)谁来负责创建 VIPER 模块:是 Router 还是 Presenter?

为何要在意架构的选择呢?

因为如果你不在意的话,难保一天,你就需要去调试一个巨大无比又有着各种问题的类,然后你会发现在这个类里面,你完全就找不到也修复不了任何 bug。一般来说,把这么大的一个类作为整体放在脑子里记着是一件非常困难的事情,你总是难免会忘掉一些比较重要的细节。如果你发现在你的应用里面已经开始出现这种状况了,那你很可能遇到过下面这类问题:

这个类是一个 UIViewController 的子类。

你的数据直接保存在了 UIViewController 里面。

你的 UIViews 好像什么都没做。

你的 Model 只是一个纯粹的数据结构

你的单元测试什么都没有覆盖到

其实即便你遵循了 Apple 的设计规范,实现了Apple 的 MVC 框架,也还是一样会遇到上面这些问题;所以也没什么好失落的。Apple 的 MVC 框架有它自身的缺陷,不过这个我们后面再说。

让我们先来定义一下好的框架应该具有的特征:

用严格定义的角色,平衡的将职责划分给不同的实体。

可测性通常取决于上面说的第一点(不用太担心,如果架构何时的话,做到这点并不难)。

易用并且维护成本低。

为什么要划分?

当我们试图去理解事物的工作原理的时候,划分可以减轻我们的脑部压力。如果你觉得开发的越多,大脑就越能适应去处理复杂的工作,确实是这样。但是大脑的这种能力不是线性提高的,而且很快就会达到一个瓶颈。所以要处理复杂的事情,最好的办法还是在遵循单一责任原则的条件下,将它的职责划分到多个实体中去。

为什么要可测性?

对于那些对单元测试心存感激的人来说,应该不会有这方面的疑问:单元测试帮助他们测试出了新功能里面的错误,或者是帮他们找出了重构的一个复杂类里面的 bug。这意味着这些单元测试帮助这些开发者们在程序运行之前就发现了问题,这些问题如果被忽视的话很可能会提交到用户的设备上去;而修复这些问题,又至少需要一周左右的时间(AppStore 审核)。

为什么要易用

这块没什么好说的,直说一点:最好的代码是那些从未被写出来的代码。代码写的越少,问题就越少;所以开发者想少写点代码并不一定就是因为他懒。还有,当你想用一个比较聪明的方法的时候,全完不要忽略了它的维护成本。

MV(X) 的基本要素

现在我们面对架构设计模式的时候有了很多选择:

MVC

MVP

MVVM

VIPER

首先前三种模式都是把所有的实体归类到了下面三种分类中的一种:

Models(模型)— 数据层,或者负责处理数据的数据接口层。比如PersonPersonDataProvider

Views(视图)- 展示层(GUI)。对于 iOS 来说所有以UI开头的类基本都属于这层。

Controller/Presenter/ViewModel(控制器/展示器/视图模型)- 它是ModelView之间的胶水或者说是中间人。一般来说,当用户对View有操作时它负责去修改相应Model;当Model的值发生变化时它负责去更新对应View

将实体进行分类之后我们可以:

更好的理解

重用(主要是 View 和 Model)

对它们独立的进行测试

让我从MV(X)系列开始讲起,最后讲VIPER

MVC - 它原来的样子


在开始讨论 Apple 的 MVC 之前,我们先来看下传统的 MVC

在这种架构下,View 是无状态的,在 Model 变化的时候它只是简单的被 Controller 重绘;就像网页一样,点击了一个新的链接,整个网页就重新加载。尽管这种架构可以在 iOS 应用里面实现,但是由于 MVC 的三种实体被紧密耦合着,每一种实体都和其他两种有着联系,所以即便是实现了也没有什么意义。这种紧耦合还戏剧性的减少了它们被重用的可能,这恐怕不是你想要在自己的应用里面看到的。综上,传统 MVC 的例子我觉得也没有必要去写了。

传统的 MVC 已经不适合当下的 iOS 开发了。

Apple 的 MVC

理想


View 和 Model 之间是相互独立的,它们只通过 Controller 来相互联系。有点恼人的是 Controller 是重用性最差的,因为我们一般不会把冗杂的业务逻辑放在 Model 里面,那就只能放在 Controller 里了。

理论上看这么做貌似挺简单的,但是你有没有觉得有点不对劲?你甚至听过有人把 MVC 叫做重控制器模式。另外关于 ViewController 瘦身已经成为 iOS 开发者们热议的话题了。为什么 Apple 要沿用只是做了一点点改进的传统 MVC 架构呢?

现实


Cocoa MVC 鼓励你去写重控制器是因为 View 的整个生命周期都需要它去管理,Controller 和 View 很难做到相互独立。虽然你可以把控制器里的一些业务逻辑和数据转换的工作交给 Model,但是你再想把负担往 View 里面分摊的时候就没办法了;因为 View 的主要职责就只是讲用户的操作行为交给 Controller 去处理而已。于是 ViewController 最终就变成了所有东西的代理和数据源,甚至还负责网络请求的发起和取消,还有...剩下的你来讲。

像下面这种代码你应该不陌生吧:

var userCell = tableView.dequeueReusableCellWithIdentifier("identifier") as UserCell

userCell.configureWithUser(user)

Cell 作为一个 View 直接用 Model 来完成了自身的配置,MVC 的原则被打破了,这种情况一直存在,而且还没人觉得有什么问题。如果你是严格遵循 MVC 的话,你应该是在 ViewController 里面去配置 Cell,而不是直接将 Model 丢给 Cell,当然这样会让你的 ViewController 更重。

Cocoa MVC 被戏称为重控制器模式还是有原因的。

问题直到开始单元测试(希望你的项目里面已经有了)之后才开始显现出来。Controller 测试起来很困难,因为它和 View 耦合的太厉害,要测试它的话就需要频繁的去 mock View 和 View 的生命周期;而且按照这种架构去写控制器代码的话,业务逻辑的代码也会因为视图布局代码的原因而变得很散乱。

我们来看下面这段 playground 中的例子:

import UIKitstruct Person { // Modellet firstName: Stringlet lastName: String}class GreetingViewController : UIViewController { // View + Controllervar person: Person!let showGreetingButton = UIButton()let greetingLabel = UILabel()override func viewDidLoad() {super.viewDidLoad()self.showGreetingButton.addTarget(self, action: "didTapButton:", forControlEvents: .TouchUpInside)}func didTapButton(button: UIButton) {let greeting = "Hello" + " " + self.person.firstName + " " + self.person.lastNameself.greetingLabel.text = greeting}// layout code goes here}// Assembling of MVClet model = Person(firstName: "David", lastName: "Blaine")let view = GreetingViewController()view.person = model;

MVC 的组装,可以放在当前正在显示的 ViewController 里面

这段代码看起来不太好测试对吧?我们可以把greeting的生成方法放到一个新类GreetingModel里面去单独测试。但是我们如果不调用与 View 相关的方法的话 (viewDidLoad, didTapButton),就测试不到GreetingViewController里面任何的显示逻辑(虽然在上面这个例子里面,逻辑已经很少了);而调用的话就可能需要把所有的 View 都加载出来,这对单元测试来说太不利了。

实际上,在模拟器(比如 iPhone 4S)上运行并测试 View 的显示并不能保证在其他设备上(比如 iPad)也能良好运行。所以我建议把「Host Application」从你的单元测试配置项里移除掉,然后在不启动模拟器的情况下去跑你的单元测试。

View 和 Controller 之间的交互,并不能真正的被单元测试覆盖

综上所述,Cocoa MVC 貌似并不是一个很好的选择。但是我们还是评估一下他在各方面的表现(在文章开头有讲):

划分- View 和 Model 确实是实现了分离,但是 View 和 Controller 耦合的太厉害

可测性- 因为划分的不够清楚,所以能测的基本就只有 Model 而已

易用- 相较于其他模式,它的代码量最少。而且基本上每个人都很熟悉它,即便是没太多经验的开发者也能维护。

在这种情况下你可以选择 Cocoa MVC:你并不想在架构上花费太多的时间,而且你觉得对于你的小项目来说,花费更高的维护成本只是浪费而已。

如果你最看重的是开发速度,那么 Cocoa MVC 就是你最好的选择。

MVP - 保证了职责划分的(promises delivered) Cocoa MVC


看起来确实很像 Apple 的 MVC 对吧?确实蛮像,它的名字是MVP(被动变化的 View)。稍等...这个意思是说 Apple 的 MVC 实际上是 MVP 吗?不是的,回想一下,在 MVC 里面 View 和 Controller 是耦合紧密的,但是对于 MVP 里面的 Presenter 来讲,它完全不关注 ViewController 的生命周期,而且 View 也能被简单 mock 出来,所以在 Presenter 里面基本没什么布局相关的代码,它的职责只是通过数据和状态更新 View。

如果我跟你讲 UIViewController 在这里的角色其实是 View 你感觉如何。

在 MVP 架构里面,UIViewController 的那些子类其实是属于 View 的,而不是 Presenter。这种区别提供了极好的可测性,但是这是用开发速度的代价换来的,因为你必须要手动的去创建数据和绑定事件,像下面这段代码中做的一样:

import UIKitstruct Person { // Modellet firstName: Stringlet lastName: String}protocol GreetingView: class {func setGreeting(greeting: String)}protocol GreetingViewPresenter {init(view: GreetingView, person: Person)func showGreeting()}class GreetingPresenter : GreetingViewPresenter {unowned let view: GreetingViewlet person: Personrequired init(view: GreetingView, person: Person) {self.view = viewself.person = person}func showGreeting() {let greeting = "Hello" + " " + self.person.firstName + " " + self.person.lastNameself.view.setGreeting(greeting)}}class GreetingViewController : UIViewController, GreetingView {var presenter: GreetingViewPresenter!let showGreetingButton = UIButton()let greetingLabel = UILabel()override func viewDidLoad() {super.viewDidLoad()self.showGreetingButton.addTarget(self, action: "didTapButton:", forControlEvents: .TouchUpInside)}func didTapButton(button: UIButton) {self.presenter.showGreeting()}func setGreeting(greeting: String) {self.greetingLabel.text = greeting}// layout code goes here}// Assembling of MVPlet model = Person(firstName: "David", lastName: "Blaine")let view = GreetingViewController()let presenter = GreetingPresenter(view: view, person: model)view.presenter = presenter

关于组装方面的重要说明

MVP 架构拥有三个真正独立的分层,所以在组装的时候会有一些问题,而 MVP 也成了第一个披露了这种问题的架构。因为我们不想让 View 知道 Model 的信息,所以在当前的 ViewController(角色其实是 View)里面去进行组装肯定是不正确的,我们应该在另外的地方完成组装。比如,我们可以创建一个应用层(app-wide)的 Router 服务,让它来负责组装和 View-to-View 的转场。这个问题不仅在 MVP 中存在,在接下来要介绍的模式里面也都有这个问题。

让我们来看一下 MVP 在各方面的表现:

划分- 我们把大部分的职责都分配到了 Presenter 和 Model 里面,而 View 基本上不需要做什么(在上面的例子里面,Model 也什么都没做)。

可测性- 简直棒,我们可以通过 View 来测试大部分的业务逻辑。

易用- 就我们上面那个简单的例子来讲,代码量差不多是 MVC 架构的两倍,但是 MVP 的思路还是蛮清晰的。

MVP 架构在 iOS 中意味着极好的可测性和巨大的代码量。

MVP - 添加了数据绑定的另一个版本


还存在着另一种的 MVP - Supervising Controller MVP。这个版本的 MVP 包括了 View 和 Model 的直接绑定,与此同时 Presenter(Supervising Controller)仍然继续处理 View 上的用户操作,控制 View 的显示变化。

但是我们之前讲过,模糊的职责划分是不好的事情,比如 View 和 Model 的紧耦合。这个道理在 Cocoa 桌面应用开发上面也是一样的。

就像传统 MVC 架构一样,我找不到有什么理由需要为这个有瑕疵的架构写一个例子。

MVVM - 是 MV(X) 系列架构里面最新兴的,也是最出色的


MVVM架构是 MV(X) 里面最新的一个,让我们希望它在出现的时候已经考虑到了 MV(X) 模式之前所遇到的问题吧。

理论上来说,Model - View - ViewModel 看起来非常棒。View 和 Model 我们已经都熟悉了,中间人的角色我们也熟悉了,但是在这里中间人的角色变成了 ViewModel。

它跟 MVP 很像:

MVVM 架构把 ViewController 看做 View。

View 和 Model 之间没有紧耦合

另外,它还像 Supervising 版的 MVP 那样做了数据绑定,不过这次不是绑定 View 和 Model,而是绑定 View 和 ViewModel。

那么,iOS 里面的 ViewModel 到底是个什么东西呢?本质上来讲,他是独立于 UIKit 的, View 和 View 的状态的一个呈现(representation)。ViewModel 能主动调用对 Model 做更改,也能在 Model 更新的时候对自身进行调整,然后通过 View 和 ViewModel 之间的绑定,对 View 也进行对应的更新。

绑定

我在 MVP 的部分简单的提过这个内容,在这里让我们再延伸讨论一下。绑定这个概念源于 OS X 平台的开发,但是在 iOS 平台上面,我们并没有对应的开发工具。当然,我们也有 KVO 和 通知,但是用这些方式去做绑定不太方便。

那么,如果我们不想自己去写他们的话,下面提供了两个选择:

选一个基于 KVO 的绑定库,比如RZDataBinding或者SwiftBond

使用全量级的函数式响应编程框架,比如ReactiveCocoaRxSwift或者PromiseKit

实际上,现在提到「MVVM」你应该就会想到 ReactiveCocoa,反过来也是一样。虽然我们可以通过简单的绑定来实现 MVVM 模式,但是 ReactiveCocoa(或者同类型的框架)会让你更大限度的去理解 MVVM。

响应式编程框架也有一点不好的地方,能力越大责任越大嘛。用响应式编程用得不好的话,很容易会把事情搞得一团糟。或者这么说,如果有什么地方出错了,你需要花费更多的时间去调试。看着下面这张调用堆栈图感受一下:


在接下来的这个小例子中,用响应式框架(FRF)或者 KVO 都显得有点大刀小用,所以我们用另一种方式:直接的调用 ViewModel 的showGreeting方法去更新自己(的greeting属性),(在greeting属性的didSet回调里面)用greetingDidChange闭包函数去更新 View 的显示。

import UIKitstruct Person { // Modellet firstName: Stringlet lastName: String}protocol GreetingViewModelProtocol: class {var greeting: String? { get }var greetingDidChange: ((GreetingViewModelProtocol) -> ())? { get set } // function to call when greeting did changeinit(person: Person)func showGreeting()}class GreetingViewModel : GreetingViewModelProtocol {let person: Personvar greeting: String? {didSet {self.greetingDidChange?(self)}}var greetingDidChange: ((GreetingViewModelProtocol) -> ())?required init(person: Person) {self.person = person}func showGreeting() {self.greeting = "Hello" + " " + self.person.firstName + " " + self.person.lastName}}class GreetingViewController : UIViewController {var viewModel: GreetingViewModelProtocol! {didSet {self.viewModel.greetingDidChange = { [unowned self] viewModel inself.greetingLabel.text = viewModel.greeting}}}let showGreetingButton = UIButton()let greetingLabel = UILabel()override func viewDidLoad() {super.viewDidLoad()self.showGreetingButton.addTarget(self.viewModel, action: "showGreeting", forControlEvents: .TouchUpInside)}// layout code goes here}// Assembling of MVVMlet model = Person(firstName: "David", lastName: "Blaine")let viewModel = GreetingViewModel(person: model)let view = GreetingViewController()view.viewModel = viewModel

然后,我们再回过头来对它各方面的表现做一个评价:

划分- 这在我们的小栗子里面表现的不是很清楚,但是 MVVM 框架里面的 View 比 MVP 里面负责的事情要更多一些。因为前者是通过 ViewModel 的数据绑定来更新自身状态的,而后者只是把所有的事件统统交给 Presenter 去处理就完了,自己本身并不负责更新。

可测性- 因为 ViewModel 对 View 是一无所知的,这样我们对它的测试就变得很简单。View 应该也是能够被测试的,但是可能因为它对 UIKit 的依赖,你会直接略过它。

易用- 在我们的例子里面,它的代码量基本跟 MVP 持平,但是在实际的应用当中 MVVM 会更简洁一些。因为在 MVP 下你必须要把 View 的所有事件都交给 Presenter 去处理,而且需要手动的去更新 View 的状态;而在 MVVM 下,你只需要用绑定就可以解决。

MVVM 真的很有魅力,因为它不仅结合了上述几种框架的优点,还不需要你为视图的更新去写额外的代码(因为在 View 上已经做了数据绑定),另外它在可测性上的表现也依然很棒。

VIPER - 把搭建乐高积木的经验应用到 iOS 应用的设计上


VIPER是我们最后一个要介绍的框架,这个框架比较有趣的是它不属于任何一种 MV(X) 框架。

到目前为止,你可能觉得我们把职责划分成三层,这个颗粒度已经很不错了吧。现在 VIPER 从另一个角度对职责进行了划分,这次划分了五层

Interactor(交互器)- 包括数据(Entities)或者网络相关的业务逻辑。比如创建新的 entities 或者从服务器上获取数据;要实现这些功能,你可能会用到一些服务和管理(Services and Managers):这些可能会被误以为成是外部依赖东西,但是它们就是 VIPER 的 Interactor 模块。

Presenter(展示器)- 包括 UI(but UIKit independent)相关的业务逻辑,可以调用 Interactor 中的方法。

Entities(实体)- 纯粹的数据对象。不包括数据访问层,因为这是 Interactor 的职责。

Router(路由)- 负责 VIPER 模块之间的转场

实际上 VIPER 模块可以只是一个页面(screen),也可以是你应用里整个的用户使用流程(the whole user story)- 比如说「验证」这个功能,它可以只是一个页面,也可以是连续相关的一组页面。你的每个「乐高积木」想要有多大,都是你自己来决定的。

如果我们把 VIPER 和 MV(X) 系列做一个对比的话,我们会发现它们在职责划分上面有下面的一些区别:

Model(数据交互)的逻辑被转移到了 Interactor 里面,Entities 只是一个什么都不用做的数据结构体。

Controller/Presenter/ViewModel的职责里面,只有 UI 的展示功能被转移到了 Presenter 里面。Presenter 不具备直接更改数据的能力。

VIPER 是第一个把导航的职责单独划分出来的架构模式,负责导航的就是Router层。

如何正确的使用导航(doing routing)对于 iOS 应用开发来说是一个挑战,MV(X) 系列的架构完全就没有意识到(所以也不用处理)这个问题。

下面的这个列子并没有涉及到导航和 VIPER 模块间的转场,同样上面 MV(X) 系列架构里面也都没有涉及。

import UIKitstruct Person { // Entity (usually more complex e.g. NSManagedObject)let firstName: Stringlet lastName: String}struct GreetingData { // Transport data structure (not Entity)let greeting: Stringlet subject: String}protocol GreetingProvider {func provideGreetingData()}protocol GreetingOutput: class {func receiveGreetingData(greetingData: GreetingData)}class GreetingInteractor : GreetingProvider {weak var output: GreetingOutput!func provideGreetingData() {let person = Person(firstName: "David", lastName: "Blaine") // usually comes from data access layerlet subject = person.firstName + " " + person.lastNamelet greeting = GreetingData(greeting: "Hello", subject: subject)self.output.receiveGreetingData(greeting)}}protocol GreetingViewEventHandler {func didTapShowGreetingButton()}protocol GreetingView: class {func setGreeting(greeting: String)}class GreetingPresenter : GreetingOutput, GreetingViewEventHandler {weak var view: GreetingView!var greetingProvider: GreetingProvider!func didTapShowGreetingButton() {self.greetingProvider.provideGreetingData()}func receiveGreetingData(greetingData: GreetingData) {let greeting = greetingData.greeting + " " + greetingData.subjectself.view.setGreeting(greeting)}}class GreetingViewController : UIViewController, GreetingView {var eventHandler: GreetingViewEventHandler!let showGreetingButton = UIButton()let greetingLabel = UILabel()override func viewDidLoad() {super.viewDidLoad()self.showGreetingButton.addTarget(self, action: "didTapButton:", forControlEvents: .TouchUpInside)}func didTapButton(button: UIButton) {self.eventHandler.didTapShowGreetingButton()}func setGreeting(greeting: String) {self.greetingLabel.text = greeting}// layout code goes here}// Assembling of VIPER module, without Routerlet view = GreetingViewController()let presenter = GreetingPresenter()let interactor = GreetingInteractor()view.eventHandler = presenterpresenter.view = viewpresenter.greetingProvider = interactorinteractor.output = presenter

我们再来评价下它在各方面的表现:

划分- 毫无疑问的,VIPER 在职责划分方面是做的最好的。

可测性- 理所当然的,职责划分的越好,测试起来就越容易

易用- 最后,你可能已经猜到了,上面两点好处都是用维护性的代价换来的。一个小小的任务,可能就需要你为各种类写大量的接口。

那么,我们到底应该给「乐高」一个怎样的评价呢?

如果你在使用 VIPER 框架的时候有一种在用乐高积木搭建帝国大厦的感觉,那么你可能正在犯错误;可能对于你负责的应用来说,还没有到使用 VIPER 的时候,你应该把一些事情考虑的再简单一些。总是有一些人忽视这个问题,继续扛着大炮去打小鸟。我觉得可能是因为他们相信,虽然目前来看维护成本高的不合常理,但是至少在将来他们的应用可以从 VIPER 架构上得到回报吧。如果你也跟他们的观点一样的话,那我建议你尝试一下Generamba- 一个可以生成 VIPER 框架的工具。虽然对于我个人来讲,这感觉就像给大炮装上了一个自动瞄准系统,然后去做一件只用弹弓就能解决的事情。

结论

我们简单了解了几种架构模式,对于那些让你困惑的问题,我希望你已经找到了答案。但是毫无疑问,你应该已经意识到了,在选择架构模式这件问题上面,不存在什么银色子弹,你需要做的就是具体情况具体分析,权衡利弊而已。

因此在同一个应用里面,即便有几种混合的架构模式也是很正常的一件事情。比如:开始的时候,你用的是 MVC 架构,后来你意识到有一个特殊的页面用 MVC 做的的话维护起来会相当的麻烦;这个时候你可以只针对这一个页面用 MVVM 模式去开发,对于之前那些用 MVC 就能正常工作的页面,你完全没有必要去重构它们,因为两种架构是完全可以和睦共存的。


说说公司老项目的框架体系:老项目是基于UITableview 和 cell 进行深度定制的,在界面上的每个UI模块都是UITableviewCell,你没有看错,每个UI模块(一个整体)都是Cell,开发中,只需要用xib描述一个cell,用一个字典指定好cell中每个UI组件对应的模型中的字段,然后会自动映射数据;搞定,收工;图解框架大概如下:

Snip20150803_1.png

框架的作者充分利用了tableView,开发快速方便,控制器的代码相对较少,作者想通过一个大牛逼viewcontroller搞定一切需求,这样做确实方便,开发者们不用动什么脑筋就能写完业务;框架在设计的过程中难点在于cell的数据映射,可变cell的高度;还有就是这个大牛逼viewcontroller的封装;

优点: 开发快速  逻辑实现部分代码量少(cell通过xib描述) 代码逻辑比较清晰,易于阅读;

缺点: 灵活性很差,viewcontroller封装的功能过多;不能做出好的用户体验,模式单一;严重依赖数据模型驱动,如果网络请求失败,没有数据,界面将一片空白,连一些静态数据也显示不出来;将所有的功能通过一个类全部包装,很多时候会造成小题大做,代码冗余量大;

结论: 非常的不灵活,没有了灵活性 对客户端来说几乎是致命;如果对比一下苹果api的继承体系,不难发现上边这套框架的思想和苹果api设计思想是背离的。

简单的看一下苹果api的设计(分层封装共性功能、分支细化小功能)

UIView的继承体系:并非全部view,只是为了说明问题,画图不是很专业,海涵

在iOS当中,所有的视图都从一个叫做UIVIew的基类派生而来

Snip20150803_3.png

看上去像是一个层级关系树管理了一些矩形块。

viewController的继承体系

Snip20150803_4.png

tips: 如果你是iOS初学者,我推荐在学习过程中通过查看头文件整理出如图的继承体系,然后开始系统的学习(例如你要学习CALayer,你可以整理出CALayer和它子类的继承体系: CATransformLayer, CATextLayer, CAShapeLayer..., 然后逐个突破学习),这样当你学会了UIControl中的一个方法:- (void)addTarget:(id)target action:(SEL)action forControlEvents:(UIControlEvents)controlEvents; 就知道了所有继承自UIControl的view都具有该方法(举个例子); 苹果api中类似这样的图可以画出好多张,CAAnimation体系,controller体系,CALayer体系...

回到正题,在编程界,你会经常看到分层这件事,其实整个计算机学科里甚至是整个人类科学中,分层设计都是一个很重要的概念,我最早把分层当回事是在大学时期的计算机网络课程中,在网络领域中众所周知的OSI参考模型体系标准,将网络分成了7层架构的参考模型,后来又被TCP/IP参考模型分为了我们所熟知的4层网络体系架构;

分层设计一个很明显的好处是:分层可以梯度的降低问题的复杂度,简化问题,也是解决某些难题的突破口;分层后的每一层功能相对单一,实现起来容易了许多;

另一个显而易见的好处是:容易拓展功能和后期的维护,由于分层后每层的职责单一,相互直接依赖降低了,装逼的说法就是耦合度低了,但是不能说相互完全不受影响,不依赖,毕竟“耦合”的藕断丝连

其他好处...

通过上边两张图,大体掌握了苹果api中的View家族的设计:UIView直接派生出3个大方向的View:

能滚动的view : UIScrollView及其子类

需要交互的view: UIControl及其子类

只是简单的展示数据,不需要交互也不需要滚动,例如UILabel....

这样梳理 我们就很清楚苹果UIView的划分体系了,掌握起来就没那么困难了;这其中的分层方式,思想,值得我们去学习、体会、实践,毕竟我们每个人都是站在巨人的肩膀上编程的。

另一种分层方式 MVC  MVVM...派系

这种分层不是对某个家族进行分层,不像UIView或者Core Animation属于家族分层(说白了就是: "谁应该是谁的儿子"的问题),

MVC  MVVM 是对整个项目代码组织结构的划分,它的规则是为了指定你的代码应该写在哪个文件中,你应该如何处理项目中代码的整体结构,这更像是三国时期的魏蜀吴,地盘到底该怎么分?

使用mvc开发iOS程序的一个问题是,model层太单薄(这货几乎什么都没干),controller中堆了大量的逻辑,导致代码成千上万行...,于是就考虑能不能model层直接承担UI特定的接口和属性(个人愚见),于是就整出了:Model-View-ViewModel 的概念。我没用过,所以理解有限,但是可以肯定的是MVVM是代码如何组织和结构化的另一种形式,至于适不适合iOS开发,怎么在iOS开发中使用,又是另一个大的话题了。

想明白这些东西后,如你所见,框架没什么神秘高深的,就是如何组织代码,整体和局部如何分层,然后就开始自己的代码封装之路;

搭建项目,先搞定网络层: AFN + MJExtension的最佳实践

要不要对AFN进行封装?

AFN本来用起来已经很方便了,如果你的项目规模很小,接口十几到二十个,页面也没多少,这样的话,没必要了。如果项目规模较大,有几十个控制器,每个控制器中都有个AFHTTPRequestOperationManager,如果AFN升级或者想要换其他的网络库,或者做一些统一处理,那么工作量就来了,这种情况就很有必要对AFN进行二次封装。

封装思路

封装AFN,即屏蔽AFN在你的代码的侵入性,所以要将AFN统一处理到一个类中,也就是一个类 has a AFN;

切割url,针对不同的host进行切割url,实际开发中,接口可能来自不同的host,即使是单一host请求,也得经常在demo 和 dev、online之间切换host,最终可以将host主机地址处理到plist文件中,方便切换,由此,我们额外需要一个解析plist文件的工具类;

AFN请求回来的数据默认都是字典,我们不想面向字典开发,希望网络层在请求完数据后直接转成模型,引入MJExtension(字典转模型框架),用模型是面向对象编程的一种思想,用模型可以用点语法,避免了字典中key的字符串拼写错误,必要时请求参数也可以用模型,开发中有时候会因为在字典中插入某个空值,导致应用程序崩溃,如果我们先用模型写好请求参数,然后通过MJExtension在将模型转为字典,就能有效避免这个问题,对于模型中为nil的字段,MJExtension会自动过滤,前提是字段是非基本数据类型;

AFN请求回调的block有个成功的block和失败的block,两个block代码有些繁琐,事实上我们仅关心成功后的有效数据和失败后的错误信息,所以将两个block合并在一个block中,通过两个参数分别接收有效数据和错误信息,并且这个参数的值是否存在必定是互斥的,即如果有效数据有值,错误信息必定是nil,反之亦然;

这部分内容较多,这里简单演示下封装后使用的精简程度,具体说明和demo可以前往github,有详细说明,欢迎交流,共同进步。

业务类接口的实现

// .h文件

+(void)getDemoDataWithResponseHandler:(responseHandler)Handler;

// .m实现文件

+(void)getDemoDataWithResponseHandler:(responseHandler)Handler

{

[self getWithUrl:demoDataUrl param:nil resultClass:[DemoAllData class] responseBlock:Handler];

}

控制器中的使用

-(void)loadNetData

{

[AppDemoServices getDemoDataWithResponseHandler:^(DemoAllData *dataObj, NSError *error) {

if (dataObj) {

[self.datas removeAllObjects];

[self.datas addObjectsFromArray:dataObj.data];

[self.tableView reloadData];

} else {

NSLog(@"网络请求发生错误");

}

}];

}

说明:笔者工作经验并不是很丰富,文章也是学习成长的一些总结和感受,如果觉得觉得水准太差,还请多多指教;

接下来:子view的点击事件与控制器之间的通信处理引发的一系列问题

Snip20150806_11.png

图中的灰色背景的View内部有两个按钮;虽然简单但是能说明一些问题;

对于已经添加在控制器view中的视图,如果还要对其引用(使用property),最好用weak 弱指针引用;

@property (weak, nonatomic) UIView *lightGrayView;

因为视图已经加在了控制器的view中,控制器的view已经对其强引用,控制器又被导航控制器...最后application在管理着他们,所以你在引用时没必要用strong引用;

小插曲播完,再回到正题。图中的灰色view,在开发中很常见,即使比这个复杂许多的view,分析切割后,缩影就是这样;对于创建这个灰色view的,我个人习惯是单独封装到一个view类中,除非这个view特别简单;单独将封装灰色view,又涉及到前边说的代码分层问题。

基于MVC,在控制器中只是对这个view的填充数据,有时候可能会处理交互;

代码层次清晰,封装后可以提高复用率,便于维护;

能大规模减少控制器的代码行数;

封装后面临另外一个问题,就是事件交互,由于控制器并不涉及灰色view内部按钮的创建代码,所以不能直接监听到灰色view内部按钮的点击事件,需要传递事件;

iOS中不同对象间事件传递方式有3种:block代理通知;  其实这里称为代理个人感觉并不是非常合适,代理是一种设计模式,很多语言开发中都会用到,比较广义;iOS中的对象间交互多为"数据传递" 和 "事件传递",或者叫"数据源"和"委托",通过tableview的datasource和delegate可以体会到,为了方便交流很多人都称为代理,而实现这两种模式的基石就是协议; 貌似整个cocoa框架都是基于协议建立起来的;所以我们自己写的时候也尽量多用协议来完成通信,能很好的和cocoa代码想融合;

如果写协议方法和使用委托就不多说了,需要注意的是,委托对象(delegate)要用weak来解除保留环;

// 声明委托对象属性

@property (weak, nonatomic) id clickDelegate;

对于调用委托方法,通常都是这么写的:

if ([self.clickDelegate respondsToSelector:@selector(goActionWithName:withObject:withSender:)]) {

[self.clickDelegate goActionWithName:strName withObject:obj withSender:sender];

};

这里如果委托对象为nil,给空对象发送消息,if条件为false,所以不会执行条件体,并不会导致程序崩溃,所以不需要在条件中先判断委托对象是否存在;

问题在于,如果定义的协议中方法较多,且多为可选实现,那么会写出一大堆这样的代码,而频繁的执行if判断除了第一次有用,后边的if检查基本上是多余的,因为一个委托对象一旦指定了几乎不会改变;所以这里可以缓存检查结果,来提升一些执行效率;

tips--缓存: 可以在delegate的set方法中实现缓存(缓存委托对象是否响应某个方法的结果),如果委托对象不发生改变,set方法只会执行一次,如果改了代理对象,也肯定会重新调用set进行赋值,所以在delegate的setter中检查协议方法和并且缓存检查结果自然是不错的方案;

关于缓存最好方式是通过二进制位,程序执行效率和资源消耗本来就是一个权衡问题,想提升程序执行效率,必然会对内存产生一定的消耗,所以很多情况下我们要权衡利弊;这里委托对象是否响应某个方法的结果只有两种情况 "响应" 和 “未响应”;一个二进制位刚好;定义这个二进制位,可以使用c语言中的 "位域";

struct {

unsigned int delegateMethod1: 1;

...

}

协议中的每个方法对应一个二进制位,进行缓存;

这里的缓存并没有对执行效率有明显的提升,现在手机的硬件能力都有很大的冗余,如果过分在乎性能这事,对开发人员来说要增加很多额外的工作量;

相信未来可能会通过强大硬件冗余来弥补的性能问题,让开发人员专心做好业务,而不用担心性能问题;

我有这样的观点的原因是:不管怎样,科技的进步都会以人为本,都是想给人类提供方便(原谅人类就是这么自私),开发人员当然也是人了,所以产生上述的观点...

这部分内容比较多,稍微缓缓...

好,继续回想一下上面的图,灰色的view中有两个按钮,协议方法为了区分点了哪个按钮,需要一个参数记录点击了哪一个按钮,区分这个可以通过tag;

-(void)grayView:(LigthGrayView *)grayView didClickAtIndex:(NSUInteger)index;

为了提高代码的可读性,我们通常要不直接传递控件的tag,而是定义枚举,然后将枚举绑定到控件的tag上,用枚举来消除魔法数字,增强代码的可读性;多数情况下你最好这么干,因为苹果的api中经常这么干,如果你打算用枚举,请一定注意命名,命名不好的枚举用起来让人很不舒服,你可不要小看命名这件事,我记得有位计算机科学家说过:“在计算机科学中只有两件难事:缓存和命名”,关于如何定义枚举和命名这里就不再赘述,实在不行,看看苹果在它的api中是如何使用和命名的,模仿它就不会有太大问题;

使用枚举消除魔法数字后,似乎代码很漂亮,很完美,符合规范;心里一阵开心‘我写的代码怎么就这么规范呢?’,就这样我写了一段时间的代码后发现一件烦人的事情:

类似这个情况太多的时候,代码中定义了大量的协议,用了大量的代理,而为了可读性我又写了大量的枚举,有时候一个控制器遵循了若干协议,每个协议都有需要实现的方法,代码量就多了,而且结构性不强,方法分散,很多时候回头review时,忘了某个方法到底是哪个协议里的;时间久了,发现这其实是一件很没有技术含量的体力活;而且多人开发的时候,有些开发人员并不会对枚举命名严格要求,很多时候看到枚举你还是不知道他是什么意思,这是个现实问题...面对现实问题,我们要灵活的处理。

于是就思考能不能不用每次都写协议,不用每次都写self.clickDelegate respondsToSelector someSel,毕竟我们处理的只是将点击事件传给控制器;

我的解决办法

定义一个公用协议,控制器监听内部view的点击事件都通过公用协议的方法;

// view点击事件的协议

@protocol YKViewClickProtcol

@optional

-(void)goActionWithName:(NSString*)strName withObject:(id)obj withSender:(id)sender;

@end

第一个参数strName用来区分点击事件来自哪个子View,这个字符串可以使用类名传递的,因为类名在一个项目中是独一无二的,也不用动脑筋考虑命名问题;

第二个参数obj,有时候需要将数据传给控制器,方便做一些处理;

第三个参数sender用来区分子view中多个点击事件;

其实这个方法的定义不符合规范,如果看苹果的代理方法,一条原则是代理方法的第一个参数是将源对象本身传出去,这可能会是个问题;

简单的自定义一个UIView作为基础view;

@interface YKView : UIView

@property (weak, nonatomic) id clickDelegate;

-(void)viewActionWithName:(NSString*)strName withObject:(id)obj withSender:(id)sender;

@end

在YKView中定义一个和代理方法很像的方法,这个方法需要暴露在.h文件,以供子类使用;

viewActionWithName...方法的实现

-(void)viewActionWithName:(NSString*)strName withObject:(id)obj withSender:(id)sender

{

if ([self.clickDelegate respondsToSelector:@selector(goActionWithName:withObject:withSender:)]) {

[self.clickDelegate goActionWithName:strName withObject:obj withSender:sender];

};

}

至此self.clickDelegate respondsToSelector ...在整个项目你只需要写一遍即可;还有一个好处是控制器的代码结构会很强,所有的子view点击事件都在同一个地方处理-(void)goActionWithName:(NSString*)strName withObject:(id)obj withSender:(id)sender;

我也尝试过更为极端的方式

// 获取在你眼前的控制器

+(UIViewController *)getLastActivityController

{

UIViewController *vController = [UIApplication sharedApplication].keyWindow.rootViewController;

if ([vController isKindOfClass:[YKTabBarController class]]) {

YKTabBarController *tabVc = (YKTabBarController *)vController;

YKNavigationController *navVc = (YKNavigationController *)tabVc.selectedViewController;

return navVc.visibleViewController;

}

if ([vController isKindOfClass:[YKNavigationController class]]) {

YKNavigationController *navVc = (YKNavigationController *)vController;

return navVc.visibleViewController;

}

return vController;

}

这个方法是用来获取最上层(或者叫正在活动)的控制器,然后强行指定clickDelegate为此控制器,因为理论上讲你能点到的view必定在最外层的控制器中,这么干能少写一条指定代理的赋值语句;使用中也没有出现错误,但是有点极端了(点到为止即可),并不是没次的点击事件都需要传递给控制器处理;

例子

Snip20150808_4.png

假设TestOneView和TestTwoView中的按钮点击事件都需要传递给控制器;

TestOneView和TestTwoView继承YKView,传递按钮点击事件只需要一行代码;

- (IBAction)btnClick:(UIButton *)sender {

[self viewActionWithName:NSStringFromClass([self class]) withObject:nil withSender:nil];

}

可能你断片了,把YKView中的处理代码回忆一下。

-(void)viewActionWithName:(NSString*)strName withObject:(id)obj withSender:(id)sender

{

if ([self.clickDelegate respondsToSelector:@selector(goActionWithName:withObject:withSender:)]) {

[self.clickDelegate goActionWithName:strName withObject:obj withSender:sender];

};

}

ViewController处理代码

- (void)viewDidLoad {

[super viewDidLoad];

CGFloat viewW = self.view.frame.size.width;

TestOneView *oneV = [TestOneView oneView];

oneV.clickDelegate = self;

oneV.frame = CGRectMake(0, 74, viewW, 200);

TestTwoView *twoV = [TestTwoView twoView];

twoV.frame = CGRectMake(0, CGRectGetMaxY(oneV.frame) + 10, viewW, 200);

twoV.clickDelegate = self;

[self.view addSubview:oneV];

[self.view addSubview:twoV];

}

#pragma mark- 子view点击事件都在这里处理 -

-(void)goActionWithName:(NSString *)strName withObject:(id)obj withSender:(id)sender

{

if ([strName isEqualToString:@"TestOneView"]) {

NSLog(@"oneView--didClick innerView");

} else if ([strName isEqualToString:@"TestTwoView"]) {

NSLog(@"TwoTwoView--didClick innerView");

}

}

示例只是为了说明问题,如果TestOneView中又多个事件交互需要传递,可以通过参数sender区分,需要传递其他数据可以通过obj;

代码量少,能节省不少时间,不用总是定义协议,检测代理,定义枚举,一些没技术含量的体力活;

事件处理在控制器中统一在一个方法,代码不在松散,review的时候能快速定位代码;




MVC

经典就是经典,没有之一。iOS中MVC架构,看懂斯坦福大学白胡子老头这张图基本上就可以了。

斯坦福大学MVC架构.png

简单理解,就是Controller对象拥有View和Model对象,两者通过Controller进行沟通。对于单个页面,三个类就搞定了,感觉很简单。

MVC.jpg

网络连接应该放在哪里?Model中吗?感觉很有道理?实际上,很多的网络连接的发起和接收后的处理都放在了Controller中,因为方便嘛。Model一般只有属性定义,没有实现。

View应该是独立一块了吧?实际上呢,View大多都放在了Controller中,有个loadView函数,很方便啊。有几个人会单独写个类来作为view?

本来,xib和Storyboard是很好的分离view的方式。但是,由于“不合适多人合作,版本管理”,非要代码写界面,还振振有词:“性能高,对培养新人有好处”。“谎言说100次都能成真话”,何况这些理由听上去还那么有理。

像“检查用户名是否合法,检查密码对不对”应该放在哪里呢?有几个人会像斯坦福大学白胡子老头那样新起一个类来写?基本上都是Controller中搞定。

BaseController,BaseView,BaseModel一定见过不少吧?有的还有好几层呢

公共View,各种名字带common或者类似的类常见吧?里面网络连接,数据库,逻辑等等往往比View本身很多,俨然一个小模块了,功能比Controller都强大了。这还是view吗?

本来MVC理论上是最简单的架构,但是实际结果呢,变成了最难懂的架构。Controller成了上帝类,什么都干。“只知道那一坨东西有用,但看不出那是简单的MVC”。

MVC也被称之为 Massive View Controller(重量级视图控制器)。其实这不是MVC的错,只是没有程序员承认自己懒惰,编程习惯不好罢了。如果能够像斯坦福大学白胡子老爷爷那样好的编程习惯,那么大部分的iOS程序都能有清晰的MVC架构。

MVVM

认识MVVM的起点是@objc上文章MVVM 介绍

MVVM来自MVC,一张经典的图就是下面这张,在好多文章中看到过。

MVVM.gif

“稍微考虑一下,虽然 View 和 View Controller 是技术上不同的组件,但它们几乎总是手牵手在一起,成对的。你什么时候看到一个 View 能够与不同 View Controller 配对?或者反过来?所以,为什么不正规化它们的连接呢?” ------ 这段话当时给我的印象很深刻,这个观点到现在我都认可。

Controller代表了一个场景(Scene)的生命周期,是一个调度者。什么都是,因为什么都离不开它。又好像什么都不是,因为它代表不了任何具体的东西。让它和View合在一起,作为广义的view就有了具体的意义,并限制了它无所不能的印象。这点值得肯定。

“显示逻辑(presentation logic)”可以从Controller中移到ViewModel中,从而给Controller减负。这个观点我也是支持的。并且我以此认为ViewModel就是用来做“显示逻辑”的,一个页面一个,随页面而变化。在Swift中,我用结构体来做ViewModel。

关于绑定机制,文章中推荐ReactiveCocoa。我去大致看了一下,主要是将KVO,Block,notification,delegate等各种通讯机制统一为RACSignal,将界面和数据进行双向绑定,功能确实强大。但是这个风格和普通iOS的开发习惯差距比较大,一下子很难变过来。文章中也说只是推荐,不强求,所以我也就一直没有采用。被误解的MVC和被神化的MVVM

至于绑定机制,在Swift中可以使用属性观察者。ViewModel一般作为Controller的一个属性,对它进行观察,一旦变化,就用ViewModel新的值设置界面元素,感觉挺好用的。还有一些相隔很远或者一对多的变化,一般可以采用NSNotification来达到目的。

从网络取数据,业务逻辑(相对于显示逻辑),应该放在那里呢?文章中没有说,看意思是保留在Controller中。还有的观点认为应该放在ViewModel中,这当然有道理,而且这是主流的理解。但是这样会让ViewModel变成另外一个上帝类。

我理解的MVVM

这是本人的理解,仅仅一家之言。主流的观点没有Logic那个类,从图中删除基本上就是了。ViewModel将是替代Controller的一个上帝类。

MVVM.jpg

Controller主要作为调度者,居于中心位置。客串部分View相关功能:比如动画里面关于view的位置改变,这些代码是要放在Controller里面的。这也符合Controller+View实现view功能的概念。

ViewModel专门做“显示逻辑”,并且用属性观察者做绑定,必要的时候用Notification。正向的绑定比如“action-target”响应就保留在Controller中,具体事情交个其他类做就可以了。

在Swift中,ViewModel和Model推荐用Struct;Logic倾向于用class。从一个简单直观的概念来说,ViewModel需要保持轻量级,跟随页面走,随时准备修改。Model也是轻量级,跟随后台API定义走,只是个数据结构,随时准备修改。而Logic就显得比较大,考虑稳定,考虑复用。

增加Logic类,负责业务逻辑,比如从网络取数据,修改数据库,检查用户名合法性,具体的响应逻辑,监听后的具体处理等等

猿题库 iOS 客户端架构设计

文章中的DataController相当于这里的Logic

重点是Controller减负,尽量起调度者职能,具体工作都放到Logic中处理。

Logic考虑复用,可以对应单个页面,也可以多个页面共用。按照业务逻辑的思路去划分模块。划分标准可以和页面分类标准不一样。

对于复杂页面,View和ViewModel可以多个,按照组件的模式去考虑。

对于表格,ViewModel对应的是表格的cell,dataSource数组中放ViewModel的序列。

表格的delegate和dataSource,目前来看,放在Controller中是最方便的。当然,为了给Controller减负,再新增一个类TableDelegate也是很不错的方法。

如果表格包含在一个组件中,用容器view做delegate和dataSource是一个不错的选择。

主要想法就是想方设法“架空”Controller,让它只做一个调度者,管理页面的生命周期就可以了。实在非它不可的时候,才让它做具体的事情。

不引入ReactiveCocoa等庞大的第三方库。这里有一篇文章不错,值得学习一下。

ReactiveCocoa 和 MVVM 入门

VIPER

这是比MVVM分类更细的一种模式。

经典图形

VIPER.png

View: 也是View + Controller

Present:相当于ViewModel,叫展示器

Interactor:交互器,侧重于业务逻辑;从网络取数据,数据库等功能都在这里。

Entity:就是Model,仅仅是数据定义

WireFrame:就是Router,是页面跳转

值得借鉴的地方

将页面跳转独立出来,做成公共模块

将业务逻辑独立出来,做成公共模块

如果只是对于单个页面,分这么多类,感觉有点啰嗦了。不够对于多页面的模块来说,还是有借鉴意义的

参考文章

浅入浅出VIPER设计架构

VIPER实践(上)

VIPER实践(下)

其他架构

一些实际在用,但是没有通用缩写名称的架构

分层模式

分层架构.png

将服务service的概念引入客户端,作为一个中间层,进行隔离

业务逻辑作为服务模块,通过服务的方式进行访问

数据访问跟业务逻辑,表现层分开,是跟后台的接口层

表现层只关注UI,相当于VIPER中的V和P;或者是MVVM中的ViewModel(仅显示逻辑)和View,但是没有双向绑定;

平台模式

平台架构.png

当前的APP,大多数是Native和H5的混合,将两者的接口代码统一成通用模块是比较好的做法

插件化也是一个越来越普遍的趋势,比如分享,第三方登录,支付等等,都由第三方以插件的形式提供。对这些插件集中管理也是好的做法

APP随着公司发展壮大,分出不同的事业部,在同一公司多个APP或者不同业务;这样就有两个相反的发展趋势:一方面,想共用模块,让多个业务共享;另一方面,各自业务又要隔离,独立发展。公司也有可能成立公共的平台部。各业务部门之间是纵向拆分;业务和平台之间是横向拆分。这就导致二维划分的立体架构。

参考文章

移动App架构设计

分离出界面层,尽量薄,和UI同学协作,快速应变

分离数据层,尽量薄,与后台合作,快速应变

一些思考

架构设计没有统一的标准,上面接触到的架构模型,都有积极的参考意义,但是都不能照搬。需要根据自己的实际情况进行一定的权衡取舍

Step0:平台型应用

以URL的方式,由主App调用子App

形式类似于调用打电话,发短信,发邮件

URL的定义需要统筹考虑

Step1:纵向划分

分Native,H5,插件三部分

Native和H5之间提供统一的桥接模块

Native和插件之间提供统一的桥架模块

如果加入ReactNative,那么也要提供Native和ReactNative之间的桥接模块。这个可以先预留,也可以以后再添加。

Step2:横向划分

Native部分进行横划分,因为这一块是最耗资源的部分

最上层是界面层(名字可以叫表现层或者UI层),这里可以借用MVVM的思想。M不用考虑,由下层以服务的形式提供。VM仅仅做显示逻辑,在Swift中用struct。这一层是跟产品的交流层,尽量薄,并且能够快速应对变化。业务逻辑等能分出去的功能,一律分出去。核心和重点就是让Controller只做调度者,万不得已可以酌情参与很少一部分的view工作。

最底层是微服务层(micro service)。这一层提供基本的功能,比如网络,缓存,加解密,系统信息,日志,统计等等。微服务的概念是只能供其他模块调用,不能调用其他模块的服务。本层中的模块之间也不能相互调用。这里是一些基础的组件,按照功能划分,相互间的隔离是第一考虑要素。要求高内聚。

中间是服务层(service),这里的服务可以调用微服务,也可以相互之间调用。

分三层相对简单一点。当然也可以分出一些接口层,服务层还可以分出公共服务层,业务逻辑层等。这个可以根据需要灵活配置。但是总体上分三层(界面、服务、微服务)。

不要跨层调用,界面层只能调用服务层提供的服务。服务层可以自己完成工作,也可以调用其他服务或者微服务完成工作。

Step3:层内划分

界面层:按照页面进行组织,提供公共的UI组件,可以理解为(M)VVM。VM作为将“界面显示”转换为“数据操作”的媒介,利用Swift的属性观察者特性,进行一级绑定。不引入RxSwift等函数式编程的大型第三方库。

服务层:分为公共服务,跳转逻辑,业务逻辑等模块,按照逻辑功能划分。跟具体页面不必相关,跟界面层的接口为各ViewModel种定义的协议。

微服务层:按照功能划分,不设计业务逻辑,分网络、数据库、加解密,日志,统计等功能

框架图

模块.jpg

语言选择Swift,最低支持版本iOS8,有条件的从iOS9开始

服务和微服务都以framework的形式提供,模块间的隔离需要重点考虑。

服务service和微服务仅仅是逻辑上的层次结构,在具体的工程组织上,都采用一级framework封装,相互间的层级和调用采用相互间的依赖隐含表示。

提供一个界面隔离的service.framework,界面层只调用它完成所有任务。作用相当于Foundation。

以workspace的方式组织工程,第三方管理工具采用Carthage。有条件的情况下,微服务以及部分服务可以采用私有Carthage的形式,更方便复用。

插件也要求以framework的形式提供,不接受.a的静态库。

如果暂时需要用到Object-C、C、C++,都统一成framework的形式,以后逐步用Swift替换。

类图

架构类图.jpg

界面层

AppDelegate、ViewController仅仅作为调度者存在,不做任何具体的事情。

ViewModel仅仅做显示逻辑相关的事情,仅仅起到将界面转换为数据的作用。用结构体struct,每个成员都是普通变量,并且都有默认值,代表了页面的确定性。ViewModel是一种数据结构,做显示逻辑的事情。

UI组件仅由View和ViewModel组成,只能包含显示逻辑,不能包含跳转逻辑,业务逻辑等。

ViewModel放UI层和Service层都有一定道理:放UI层表示显示逻辑;放service层表示UI和service之间的数据接口。考虑再三,觉得还是应该放UI层。ViewModel最大的作用还是在于将UI变化转变为数据操作,本质上离UI应该更近一些。

至于UI层和Service层之间的接口,还是定义相关的的ViewModel protocol比较好。这些protocol的定义放在service中(由于framework的影响),将ViewModel的一些基本需求放在protocol中。service中用Model或者还是用其他来满足这个protocol,就不做要求了。用protocol作为接口比单纯用Model做接口要好。因为Model随着后台API定义而变,而protocol只相当于一个基类,类型更灵活。

ViewModel.jpg

除了定义一个ViewModel的protocol之外,再定义一个是service的protocol,(既然引入service概念,就可以淡化logic和data的概念)。

界面层保持最轻量级。页面跳转逻辑,具体业务逻辑等工作全部下沉到服务层来做。

ViewModel是一个struct,主要做显示逻辑,概念相对比较小,一般一个页面一个或者多个。而service是一个类,概念比较大,可以多个页面共用一个。尺度可以根据具体情况灵活掌握。不同的页面,通过扩展遵循不同的协议区分开来。类本身可能比较大,但是每一部分都是相对比较小的。

Protocol.jpg

服务层

service.framework作为一个粘合层存在,AppDelegate、ViewController只要import service就可以调用相关服务了。

金融.framework、保险.framework等属于业务特有的逻辑

Router、用户、分享等属于业务无关的公共逻辑

层内的各模块间可以相互调用

具体存在形式,单例、类、或者framework等,可根据具体情况灵活决定。

为了结构清晰,图中的调用关系线只画出了很少的一部分,大部分线都没有画出来。

微服务层

从开发的角度,按照功能分类;是工程师之间交流的技术语言,而不是跟产品交流的业务语言

功能高内聚,作为被调用的基础模块

模块之间不要存在相互调用的关系

以framework的形式存在。高内聚,高复用。

日记本