iOS Architecture Patterns —— iOS 体系结构模式

写在前面的话:第一次做技术文章翻译,没有机翻,都是自己一个个打的,有翻译问题的各位多见谅。

原文:Medium原文

参考:《猿题库 iOS 客户端架构设计》

iOS中的结构模式

——解密MVC,MVP,MVVM和VIPER——

当在iOS中使用MVC感到很奇怪?对是否切换至MVVM犹豫不决?听说过VIPER ,却不确定是否值得这么做?
如果在讨论上述问题时感到困惑,往下看,你将会找到上述问题的答案。
通过阅读这篇文章,你将会巩固自己关于iOS开发中的结构模式的知识,我们将简要的回顾一些常见的例子,并进行系统的比较,同时还会演示一些简单的例子。如果你需要了解任何一个例子的细节,你可以点击下面的链接。
掌握设计模式很可能会上瘾,请注意:在阅读这篇文章之前,你可要要停止思考如下问题:
谁更符合网络请求? 模型(Model) 还是 控制器(Controller)?
怎么把一个模型(Model)传入到一个新的视图模型(View Model)中?
谁构建了VIPER模块,路由(Router)还是展示器(Presenter)?
 screenshot

谁会考虑结构模式选择?

因为如果你选择不采用的话,某一天,当调试某一个复杂的大型类的时候,你会发现你无法定位并解决你的类中的任何一个bug。时时刻刻把一个类的整体结构考虑在内是很困难的,所以你会经常忽视一些重要的细节。如果在你的应用中你已经处于这样的处境,应该包含了以下的情况:
  • 这个类是UIViewController的子类。
  • 你的数据直接存储在UIViewController中
  • 你的UIViews基本什么没做
  • Model是一个单纯的数据结构 (dumb data)
  • 你的单元测试什么都没覆盖到
这些都会发生,可是事实上你已经遵循了Apple的指导示例并且继承了Apple的MVC样式,所以不必感到悲伤。是苹果的MVC样式存在问题,我们稍后会讲到这个问题。
让我们先明确一下一个优秀的结构所应该有的特点
  1. 通过明确角色来平衡的分配整体的责任。
  2. 易测性和第一点是相伴的(不必担心的是,当有了一个合适的结构和就会变得很容易)
  3. 方便使用并且容易维护

为什么要用分布式

当我们思考问题的解决方法时,分布式能使我们的大脑稳定负荷的工作。如果你认为不断的开发大脑能去适应了解更复杂的事物时,那么你是正确的。但是这个能力并不是线性的增长并快速地到达顶部。所以解决复杂问题的最简单地方便是将一个问题通过单一职责原则分成不同的部分,并且每个部分分担不同的责任。
 

为什么要方便测试?

在对那些庆幸已经对采用的了单元测试的人来说这并不是一个问题,因为单元测试在增加新功能或者重构一个复杂类的时候会提示失败,这就意味着这些测试能避免开发者在运行时才能发现问题,因为当出现在用户的设备上时,通常需要好几个星期才能联系到用户并解决问题。

为什么要方便使用?

这个没有什么回答的必要,但是最好的代码是从未被写过的代码,所以代码量越少,bug就越少。这就意味着少的代码量从来不代表一个程序员的懒惰,同时也不应该为了一个高大上的解决方案而忽视了它的维护陈本。

MV(x)简介

如今,当提到设计结构时便会有如下内容:
前三种设计结构把app假定分为3个类别:
  • 视图(Model):负责领域数据,可以操作数据的数据访问层,类似于‘Person’ 或者 ‘PersonDataProvider’ 的类
  • 视图(View):负责视觉呈现层(GUI),在iOS开发中的那些一UI为前缀的类
  • 控制器(Controller)/展示层(Presenter)/视图模型(ViewModel)  : Model和View之间的连接器和纽带,主要负责把用户在View上的操作相应至模型中,当Model中数据改变时会刷新View
将整体分块能够让我们:
  • 更好的理解他们(这点我们已经知道)
  • 重用他们(在ModelView上最适用)
  • 可以对他们独立测试
让我们以MV(X)模式开始,稍后再回到VIPER上。

MVC

它的前世是什么样的?

在讨论苹果版的MVC之前,我们先看一下传统的版本。
 screenshot
在这个版本中,View是无状态的。它只是当Model改动时,Controller的简单的表现而已。比如在网页上你点击了其他网页跳转时,整个界面会完全刷新。尽管很可能在iOS程序中也是继承了这种传统的MVC模式,但是并没有什么意义,因为存在结构问题——三个模块之间高度的耦合,并且每个模块都能知道另外两个模块。这会显著的降低每个模块的复用性,你并不想让这个发生在你的应用上。因此,我们放弃去写一个规范的MVC的例子。

| 传统的MVC似乎不适合现在的iOS的开发。

苹果式的MVC

预期中的

screenshot

Controller是View和Model之间的中介者,所以View和Model之间没有直接联系。最少复用的是控制器,对我们来说通常也是能接受的,因为我们需要一个地方来放那些不适合放在模型中的棘手的业务逻辑。
理论上来说,这个看上去容易接受,但是你会感觉到有些地方有问题,对吧?你是否层听说过人们把MVC成为Massive View Controller 。此外,视图控制器过载对iOS开发者来说已经成为一个非常重要的话题。如果苹果只是采用了传统的MVC,并提升了它的话,这为什么会发生呢?

苹果式的MVC

现实中的

screenshot

Cocoa Mvc 鼓励你写复杂的视图控制器,因为他们在视图的生命周期中高度耦合,所以很难说他们是相互独立的。尽管你仍然有能力把一些业务逻辑和数据改造分配到模型中,但是当有工作需要在视图处理时,通常没有更多的选择,大部分情况下视图的全部责任就是发送动作给控制器。视图控制器最后作为一个代理或者数据源,负责网络请求的分发和取消….你想让他做的任何事。
多少次你见到过如下的代码:
var userCell = tableView.dequeueReusableCellWithIdentifier(“identifier”) as UserCell
userCell.configureWithUser(user)
这个单元格的视图直接由模型配置,已经违背了MVC的原则,但是上面的这个例子经常发生,而且人们通常不觉得它是错的。如果你严格遵守MVC的话,那么你应该通过控制器来配置,不把模型直接传给视图,并且这个会把你的控制器的变的更为庞大。

|    Cocoa Mvc 被扩充为 Massive View Controller (复杂的视图控制器)也是有理由的。

不涉及到单元测试(希望你这么做了)的话,这个问题还是不明显的。因为你的视图和控制器紧密的联系在一起,它会变的很难测试,因为你必须在模仿视图和生命周期时十分有创造性,通过这样的方式来写控制器的代码,你的业务逻辑代码就能尽可能的从视图中分离出来。
让我们来看下这个简单的 playground 例子:

import UIKit

struct Person { // Model

let firstName: String

let lastName: String

}

class GreetingViewController : UIViewController { // View + Controller

var 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.lastName

self.greetingLabel.text = greeting

}

// layout code goes here

}

// Assembling of MVC

let model = Person(firstName: “David”, lastName: “Blaine”)

let view = GreetingViewController()

view.person = model

|    MVC的组成可以通过显示的视图控制器来展现

这看起来不是很容易测试吧?我们可以Greeting的构造放入新的GreetingModel类中并且对他进行单独的测试,但是如果在GreetingView的控制器中不调用视图里那些会加载所有视图的方法(viewDidLoad, didTapButton),我们不能测试任何的展示逻辑(虽然在上面的例子里面没有很多类似的逻辑),这对单元测试来说很不好。
事实上,在一个模拟器(iphone4s)上加载和测试UIView并不能保证它能够在其他设备(iPad)上正常工作,所以我建议在Uni Test Target配置中去掉“Hostr application” ,同时在模拟器上运行你的测试时不要运行你的应用。

|    视图和控制器之间的相互关系不是真正适合进行单元测试的

总而言之,看上去Cocoa MVC似乎是一个非常差的模式。但是让我们来根据在文章开头定义的那些特点来进行评估:
分散性——视图和模型事实上是独立的,但是视图和控制器确实紧密联合在一起。
可测试性——由于之前较差的分散性,你可能只能测试你的模型。
方便性——和其他模式相比最少的代码量,而且所有人对这个模式都很熟悉,并且,对没有经验的开发者来说也很容易维护。
如果你不准备投入太多时间在设计结构中或者对你的小小工程来说不会有高的维护陈本的话,那么Cocoa MOV 是适合你的选择。

|    Cocoa MVC 是最适合快速开发的设计结构

MVP

Cocoa MVC理想模式的实现

screenshot

它是不是看上去和苹果的MVC十分相似?是的,它就是这样,而且它叫做MVP(被动视图的变体)。但是。。。苹果式的MVC 真正意义上是MVP么?不是,回想之前的视图是与控制器高度耦合的,然后MVP中的中间者——Presesnt,他与视图控制器的生命周期没有任何关系,并且视图可以轻易的重用,在Presenter中没有任何的布局代码,但是他负责为View刷新状态和数据。
screenshot

|    如果我告诉你,UIViewController 就是 View

MVP中,UIViewController的子类事实上是Views而不是Presenters。这个区别提供了极好的可测试性,但是你必须降低开发速度,因为你必须手动写上数据和事件的绑定,就像你在下面例子中看到的。
importUIKitstruct Person { // Model
let firstName: String
let lastName: String
}protocol GreetingView: class {
func setGreeting(greeting: String)
}protocol GreetingViewPresenter {
init(view: GreetingView, person: Person)
func showGreeting()
}class GreetingPresenter : GreetingViewPresenter {
unowned let view: GreetingView
let person: Person
required init(view: GreetingView, person: Person) {
self.view = view
self.person = person
}
func showGreeting() {
let greeting = “Hello” + ” ” + self.person.firstName + ” ” + self.person.lastName
self.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 MVP
let model = Person(firstName: “David”, lastName: “Blaine”)
let view = GreetingViewController()
let presenter = GreetingPresenter(view: view, person: model)
view.presenter = presenter

关于程序集的重要信息

MVP模式是第一个揭示了由于三个完全独立的层面导致的程序集问题。首先我们并不想让View和Model之间有联系,在展示界面的view controller中写代码是不正确的。举个例子,我们可以做app之间负责执行程序和View之间的展示的路由服务。这个问题不仅出现在MVP模式中,同样的在以下模式中:
让我们看一下MVP模式的特点
  • 分散性——我们把大部分的任务都分在Presenter和Model之间,View是不涉及逻辑业务的。(比如在上面的例子中的View)
  • 测试性——完美的,因为View不涉及业务,我们可以测试大部分的业务逻辑。
  • 方便性——在我们这个不实际的例子中,代码量是MVC的两倍,但是同时,MVP的思想很明确。

|    iOS中的MVP意味着优秀的测试性和巨大的代码量

MVP

带有绑定和提醒器

 

这是另一种样式的MVP——监视控制器的MVP. 这个模式在Presenter(监视控制器)处理来自View的事件和控制改变View的同时增加了View和Model的之间绑定。
 screenshot
但是正如我们之前所看到的,不明确的职责规划是不好的,像View和Model中的高度耦合。这个和Cocoa的桌面应用开发的模式很像。
和传统的MVC一样的是,我看不到任何值得写一个关于这个有缺陷的模式的例子的必要。

MVVM

最新,最酷炫的MV(X)思想。

MVVM是最新的MV(X)思想,希望它能够把之前遇到的问题纳入考虑解决的问题中。
理论上来讲 Model-View-ViewModel 看上去很好,ViewModel对我们来说很熟悉,但是中间者——ViewModel也是。
screenshot
他和MVP很相似:
  • MVVM中 把 ViewController 作为 View
  • 在View和Model之间没有紧密的耦合
另外,他是的连接和监督控制器版本的MVP很像,但是这次在View和Model中间并没有联系,取而代之的是在View和ViewModel 之间。
所以在iOS的实际中ViewModel到底是什么样的呢?基本上是你的View的状态和在UIKit上独立的展示。ViewModel会根据Model的改变而改变并且通过更新后的Model来刷新自己的,而且我们在View和ViewModel之间有一个绑定,View也会因此刷新。

绑定:

在MVP部分中我有提到,下面让我们来讨论一下。绑定来源于OS X 的开发盒中 ,但是在iOS的工具盒中并没有。当然我们有KVO 和 notifications,但是他们并没有绑定那么方便。
因此,假如我们不想自己来写他们,我们有两种选择:
  • 基于KVO的绑定库,类似月  RZDataBinding 或者 SwiftBond
  • 完整的底层巨大框架像: ReactiveCocoa, RxSwift 或者 PromiseKit
事实上,现在当你听到’MVVM’ 你想到 ReactiveCocoa,反过来也一样。尽管可能通过简单的绑定来构建MVVM,ReactiveCocoa能够让你重分利用MVVM。
关于reactive 框架有个痛苦的事实:强大的功能同时会带来强大的责任。当时使用Reactive时很容易把事情弄乱,换句话说,如果发生错误了,你可能要花很多的时间来debug程序,看一下下面这个堆栈信息:
 screenshot
在我们的例子中,FRF 框架 ,或者是 KVO过度复杂了,取而代之的,我们会通过使用 showGreeting 方法来让View Model刷新,使用这个简单的配置来实现 greetingDidChange的回调。
importUIKitstruct Person { // Model
let firstName: String
let lastName: String
}protocol GreetingViewModelProtocol: class {
var greeting: String? { get }
var greetingDidChange: ((GreetingViewModelProtocol) -> ())? { getset } // function to call when greeting did change
init(person: Person)
func showGreeting()
}class GreetingViewModel : GreetingViewModelProtocol {
let person: Person
var 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 = { [unownedself] viewModel in
self.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 MVVM
let model = Person(firstName: “David”, lastName: “Blaine”)
let viewModel = GreetingViewModel(person: model)
let view = GreetingViewController()
view.viewModel = viewModel
让我们再回到之前的几个特点:
  • 分散性——这个在我们的小例子中可能不是很明显,但是事实上, MVVM中的View比MVP的view承担更多的责任。因为前者通过构架绑定来根据ViewModel来刷新自己,然而后者只是把自己的所有的时间发给Presenter并不刷新自己。
  • 易测性——ViewModel和View之间没有联系,这个是我们能够方便测试。View也可以测试,但是因为他依赖于UIKit你可能会跳过这个。
  • 易用性——和我们的MVP例子中有着同样的代码量,但是在实际的app中,你必须把所有View的事件发给Presenter然后手动刷新View, 如果你使用绑定的话会代码量会更少。

|    MVVM模式非常有吸引力,因为他结合了之前提出的优点,而且有了View侧的绑定机制也不需要额外的代码来刷新View。更何况,可以很方便的测试。

VIPER

LEGO式的构建经验在iOS app 设计中的移植

VIPER 是我们最后的一个介绍对象,他很有趣,因为他不是来自于 MV(X) 的结构的。
现在,你肯定很赞同颗粒化责任是非常有用的。VIPER在分离责任的想法上进行的新的迭代,这边就有了5层的结构。
screenshot
  • Interactor——包含与数据(Entities)相关的业务逻辑或者网络相关,类似于生成一个数据的实例,然后再服务器上获取他的数据。为了实现这个例子,你通常会用一个VIPER模块外的依赖像Serveice和Managers。
  • Presenter——包含UI相关(但是基于UIKit的)的业务逻辑,调用Infractor上的方法。
  • Entities——你计划的数据对象,并不是数据获取层,因为那是Interactor的工作。
  • Router——负责VIPER 模块之间的连线。
主要的讲,VIPER模块可是一个屏幕或者你的整个应用——想想授权,可以是一个屏幕也可以是多个相关的。你的LEGO积木模块有多小呢?这还是取决于你自己。
如果我们把他和MV(X)类的进行对比,我们会发现在分散职责是有一点点不同:
  • Model(数据相关)中的逻辑切换到了Interactor中,Entities只是一个单纯的数据源(dumb structures)
  • 只有Controller/Presenter/View/Model中的UI展现的职责放到了Presenter中,并不是数据修改的能力。
  • 有了Router的处理,VIPER是第一个能够明确定位导航职责分配的模式。

|    对iOS的应用来说做好合适的路由是一个挑战,MV(X)模式就是简单的不解决这个问题

这个例子不涉及路由和模块间的交互,因为这个话题在MV(X)模式中通常都不会涉及。
importUIKitstruct Person { // Entity (usually more complex e.g. NSManagedObject)
let firstName: String
let lastName: String
}struct GreetingData { // Transport data structure (not Entity)
let greeting: String
let subject: String
}protocol GreetingProvider {
func provideGreetingData()
}protocol GreetingOutput: class {
func receiveGreetingData(greetingData: GreetingData)
}class GreetingInteractor : GreetingProvider {
weakvar output: GreetingOutput!func provideGreetingData() {
let person = Person(firstName: “David”, lastName: “Blaine”) // usually comes from data access layer
let subject = person.firstName + ” ” + person.lastName
let 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 {
weakvar view: GreetingView!
var greetingProvider: GreetingProvider!func didTapShowGreetingButton() {
self.greetingProvider.provideGreetingData()
}func receiveGreetingData(greetingData: GreetingData) {
let greeting = greetingData.greeting + ” ” + greetingData.subject
self.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 Router
let view = GreetingViewController()
let presenter = GreetingPresenter()
let interactor = GreetingInteractor()
view.eventHandler = presenter
presenter.view = view
presenter.greetingProvider = interactor
interactor.output = presenter

让我们再回到特性上来:
  • 分散性——毋庸置疑的,VIPER在分散职责上是做的最棒的。
  • 易测性——想都不用想,好的分散性 = 好的易测性
  • 易用性——基于上面的这两点,你应该也猜到了。某些小的职责你也必须在类里面写大量的接口。

所以什么是LEGO呢?

在使用VIPER的时候,你可能会觉得像是在用LEGO积木来构建帝国大厦,这也正是问题所在。可能,对你的程序来说接受VIPER可能太早了,你更愿意选择一些简单的方式。有些人忽略了这一点并且杀鸡用牛刀。我想也许是他们觉得他们的app在未来可能会收获好处,即使现在的维护成本是相当的高。如果你也这么认为的话,那么我建议你去试一下Generamba https://github.com/rambler-ios/Generamba—— 一个生成VIPER结果的工具。反正对我来说,感觉就像明明简单的一枪就能搞定的事要用一台高度自动化的大炮来做。

结论

我们总结的几种设计模式,希望能够解决你的一些疑惑,而且我相信你应该意识到了是 no sliver bullet(没有万能方法)的,所以选择构架方式在你的某些局面下需要考虑的东西。
因此,在同一个app中包含了混合的构架很正常。举个栗子:你首先使用的是MVC,然后你发现有个界面在MVC下边的开始难以维护,然后切换至了MVVM,但是只是在这一个界面。所以没有必要去重构其他MVC下正常工作的界面,因为所有的模式都是方便协同工作的。

任何事物都应尽可能简洁,但不能过于简单——爱因斯坦

DannyLau

奋斗的菜鸟程序猿【移动开发者 / iOS /coocs2dx/ Android】 电影迷/LOL/万青粉 不拖延会死星人

发表评论

电子邮件地址不会被公开。