为避免撕逼,提早声明:本文纯属翻译,仅仅是为了学习,加上水平有限,见谅!react
【原文】https://medium.com/ios-os-x-development/ios-architecture-patterns-ecba4c38de52ios
使用MVC
进行iOS
开发感受到很怪异?在切换到MVVM
的时候心存疑虑?据说过VIPER
,可是不知道是否值得采用? 读下去,这篇文章将为你一一解惑。 若是你正打算组织一下在iOS
环境下你掌握的架构模式知识体系。咱们接下来回简单地回顾几个流行的架构并作几个小的练习。关于某个例子若是你想了解的更详细一些,能够查看下方的连接。git
掌握设计模式会让人沉迷其中,全部必定要小心:相比阅读本文章以前,你可能会问更多像这样的问题: 由谁进行网络请求:Model
?仍是ViewController
?github
如何向新视图(View
)的ViewModel
中传递Model
web
由谁建立一个新的VIPER
模块:路由(Router
)?仍是展现器(Presenter
)? objective-c
由于若是你不这样作,终有一天,你在调试一个拥有着数十个不一样方法和变量(things)的庞大的类文件时,你会发现你没法找到并修复此文件中的任何问题。天然地,也很难把这个类文件当作一个总体而熟稔于心,这样你可能老是会错过一些重要的细节。若是你的应用已经处于这样的境况,颇有多是这样:编程
UIViewController
的子类。UIViewController
中。UIViews
什么都不作。Model
是哑数据结构(dumb data structure
)。dumb data structure: 只用来存储数据的结构,没有任何方法。详见:https://stackoverflow.com/questions/32944751/what-is-dumb-data-and-dumb-data-object-meanswift
让咱们定义一下一个好的架构应该有的特色:设计模式
在咱们试弄清楚事物是如何运做的时候,解耦能够保证大脑的负载均衡。若是你认为开发的(项目)越多你的大脑越能适应理解复杂的问题,那么你就是对的。可是这个能力不是线性扩展的而且很快就能达到上限。因此,解决复杂性的最简单的方式就是在多个实体间按照“单一责任原则” 拆分职责。promise
对于那些已经习惯了单元测试的人来讲这并非一个问题,由于再添加了新的特性和重构了一个复杂的类后一般会运行失败。这意味着单元测试能够帮助开发者发现一些在运行时才会出现的问题,而且这些问题常见于安装在用户的手机上的应用上,此外要修复这些问题也须要大概一周的时间。
这个问题并不须要回答,但,值得一提的是:最好的代码老是那些没有被写出来的代码。所以,代码越少,错误也就越少。这也说明,总想着写最少代码的开发者不是由于他们懒,而且你也不该该由于一个更聪明的解决方案而忽视维护成本。
##MV(X)概要 现今,当咱们说起架构设计模式的时候,咱们有不少的选择。好比:
MVC
MVP
MVVM
VIPER
上述的前三个架构采起的是,把应用中的实体(entities)放入下面三个类别中其中一个内。
Models
——负责域数据(domain data)和操做数据的数据访问层(Data access layer),可认为"Person"和"PersonDataProvider"类。Views
——负责表现层(GUI),对于iOS环境来讲就是全部以"UI"开头的类。Controller
/Presenter
/ViewModel
——模型(Model
)和视图(View
)的粘合剂、中介,一般的负责经过响应用户在视图(View
)上的操做通知模型(Model
)更新、并经过模型的改变来更新视图(View
)。实体解耦能让咱们:
View
)和模型(Model
))让咱们想看一下MV(X)
架构,以后再回过头来看VIPER
在讨论苹果的MVC架构时,先来看一下传统的MVC是什么样的。
View
)是无状态的。一旦模型(
Model
)改变视图(
View
)仅仅只是被控制器(
Controller
)渲染而已。想象一下点击一个连接导航到其余地方后网页彻底加载出来的状况。尽管,iOS应用可使用传统的MVC架构,但这并无多大意义,由于架构自己就存在问题——三个实体(
entities
)之间联系太过紧密,每个实体都知道(引用)另外两个实体。这就致使了实体的复用性急剧降低——在你的应用中这并非你所想要的。出于这个缘由,咱们就不写MVC范例了。
Traditional MVC doesn't seems to be applicable to modern iOS development. 传统MVC架构看上去并不适合用于如今的iOS开发中。
控制器(Controller
)是视图(View
)与模型(Model
)二者之间的中介,这使得视图(View
)与模型(Model
)都不知道对方的存在。控制器(Controller
)是可复用的最少的,对咱们来讲这一般很好,由于咱们必须有一个地方去放置一些不适合放在模型(Model
)中且比较棘手的逻辑。 理论上,看起来很是简单,可是你感受到有些地方不对,是否是这样?你甚至据说过人们把MVC称做Massive View Controller。此外,视图控制器"瘦身"(View Controller offloading)成了iOS开发者中的一个重要话题。若是苹果只是采用传统MVC
架构或者只是稍加改进,为何会出现这种状况?
Cocoa MVC
鼓励你使用大型的视图控制器(
Massive View Controllers),因为他们都参与到了视图(
View
)的生命周期中了以致于很难说他们是分离的。尽管你仍有能力分流一些业务逻辑和数据转换功能到模型(
Model
)中,可是当涉及到把工做分流到视图(
View
)中去时你就诶有更多的选择了,由于在大多数时候视图(
View
)的全部职责是把动做传递到控制器(
Controller
)中。视图控制器(
View Controller
)最终最为全部控件的委托和数据源,一般负责调度和取消网络请求...应有尽有
你见过多少次这样的代码:
var userCell = tableView.dequeueReusableCellWithIdentifier("identifier") as UserCell
userCell.configureWithUser(user)
复制代码
cell
这个视图是由Model直接配置数据的,所以这违反了MVC
指南,可是这种状况无时无刻不在发生着,并且一般人们并不认为这样有什么错的。若是你严格的遵照MVC
架构,那么你应该在Controller
中配置cell数据,不用把Model
传递到View
中去,这会增长控制器的大小(复杂度)。
Cocoa
MVC
is reasonably unabbreviated the Massive View Controller.Cocoa
MVC
被称做大型视图控制器是合理的。
在未说起单元测试(Unit Testing)前MVC
的问题并非很明显(但愿,你的项目中有单元测试)。因为你的视图控制器(View Controller
)与视图(View
)是紧耦合的,所以很难对其进行测试,由于你不得不很是有创造性的模拟视图和他们的生命周期,使用这种方式编写视图控制器(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:", forControlEvent: .TouchUpInside)
}
func didTapButton(button: UIButton) {
let greeting = "Hello" + " " + self.person.firstName + " " + self.person.lastName
self.greetingLabel.text = greeting
}
// 布局代码在这儿
......
}
// 组合MVC
let model = Person(firstName: "David", lastName: "Blaine")
let view = GreetingViewController()
view.person = model
复制代码
MVC assembling can be performed in the presenting view controller 组合
MVC
能够在展现视图控制器(presenting view controller
)中来完成
不是很容易测试,是否是这样?咱们能够把生成greeting
的代码放入到GreetingModel
类里并单独的进行测试,可是,在没有直接调用与UIView
有关的方法(如:viewDidLoad
, didTapButton
,这些方法可能会加载全部的视图,不利于单元测试。)的状况下,咱们没法测试GreetingViewController
中的任何展现逻辑(尽管上面的代码中没有太多这样的逻辑)。
事实上,在模拟器(如:iPhone4s
)上加载并测试视图并不能保证在其余设备(如:iPad
)上也能正常工做,因此,我建议从Unit Test目标(Unit Test target
)配置中移除主应用程序(Host Application
)并在模拟器上没有应用运行的状况下运行测试。
The interactions between the View and the Controller aren't really testable with Unit Tests. 视图和控制器之间的交互很难进行单元测试。
综上所述,Cocoa MVC
多是一个至关糟糕的选择。让咱们按照文章开头定义的特色来评估一下这种架构模式:
View
)和模型(Model
)确实解耦了,然而,视图(View
)和控制器(Controller
)倒是紧密耦合的。Model
)。若是你没有打算在架构时耗费太多时间而且以为高成本的维护费用对你的小项目来讲是一种过分的浪费的话,那么Cocoa MVC
就是你的最好选择。
Cocoa MVC is the best architectural pattern in term of the speed of the development. 在开发速度上面
Cocoa MVC
是最好的架构模式。
MVP
(Passive View variant)。等下...是否是
Apple’s MVC
事实上就是
MVP
?并非,回想一下在
MVC
中
View
和
Controller
是紧密耦合的,然而,MVP的中介——
Presenter
与View Controller的生命周期没有任何关系,而且很容易模拟View,因此
Presenter
中没有任何布局代码,可是它却负责用数据和状态更新
View
。
What if i told you,the UIViewController is the View.
我告诉你,视图控制器就是视图。
就MVP
来讲,UIViewController
的子类实际上就是视图(Views
)不是展现器(Presenters
)。这点区别带来了极好的可测试性,而花费代价是开发速度,由于你不得不手工进行数据和事件绑定,就如你在下面的例子中看到的那样:
import UIKit
struct 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: GreetingViePresenter!
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 go here
}
let model = Person(firstName: "David", lastName: "Blaine")
let view = GreetingViewController()
let presenter = GreetingPresenter(view: view, person: model)
view.presenter = presenter
复制代码
MVP
是第一个揭示出组装问题(assembly problem)的架构模式,而出现这个问题的缘由是它有三个实际上独立的层。因为咱们不想让视图(View
)了解模型(Model
),因此在展现视图控制器(也就是视图)执行组装是不正确的,所以咱们不得不在其余地方执行它。例如,咱们能够建立一个app范围(app-wide)的路由(Router
)服务,让它来完成执行组装和视图到视图(View-to-View
)的展现功能。这个问题不止在MVP
中存在,在下面介绍的其余模式中也存在。
让咱们看一下MVP
的特色:
Presenter
)和模型(Model
),还有至关简单、轻薄的视图(dumb view)(在上述例子中的模型也很简单)。MVP in iOS means superb testability and a lot of code
iOS
中的MVP
架构意味着极好的可测试性和大量的代码。
####绑定和Hooters 还有一种类型的MVP
架构模式——the Supervising Controller MVP。这个变种包括了视图和模型的直接绑定,展现器(The Supervising Controller)在处理动做的同时还能够改变视图。
可是,就如咱们已经知道的,模糊的职责拆分是不正确的,视图和模型的紧耦合也一样不可取。这和Cocoa
桌面应用开发很类似。和传统的MVC
同样,给有瑕疵的架构写例子没有任何意义。
MV(X)
类中近期最优秀的架构(The latest and the greatest of the MV(X) kind)MVVM是MV(X)这类中最新的架构形式,因此,咱们但愿它可以解决MV(X)
以前所面临的问题。
理论上,Model-View_ViewModel
这种架构很棒。不只View
和Model
,并且Mediator
——至关于View Model
,咱们都已经熟悉。
MVP
很类似:
View
)和模型(Model
)之间不存在紧耦合。另外,它还能够像MVP
那样绑定;可是绑定不是发生在视图(View
)和模型(Model
)之间,而是视图(View
)和视图模型(View Model
)之间。
那么,iOS现实中的视图模型(View Model
)的庐山面目是什么?它是你的视图及其状态的基本的UIKit
的独立展现。视图模型触发模型的改变,并利用改变后的Model
更新本身,因为咱们在视图和视图模型之间进行了绑定,视图也会根据视图模型的改变而改变。
绑定我在讲解MVP
架构部分简单的提到过,这里咱们在对其进行一些讨论。绑定是从OSX
开发而来的,并且iOS中并无这个概念。固然,咱们有KVO
和通知(notifications
),可是它的使用并无绑定方便。
因此,假若不想本身编写绑定代码,咱们还有两个选择:
RZDataBinding
或者SwiftBond
。ReactiveCocoa
、RxSwift
或者PromiseKit
`。事实上,现今,只要你听到“MVVM
”你就会想到ReactiveCocoa
,反之亦然。尽管使用简单地绑定也能够建立MVVM
架构的项目,可是,ReactiveCocoa
(或者同类的库)却可让你把使用MVVM
架构的优点最大化。
关于Reactive
库有一个残酷的现实须要面对:功能强大却伴随着巨大的职责。当使用Reactive
库的时候极容易把不少事情搞混,若是出现错误,你可能须要花费不少的时间去在APP中定位问题所在,因此看一下下图的调用堆栈。
在简单的例子中,使用FRF(functional reactive function:函数式响应式函数)库甚至KVO都显得大材小用,相反咱们显式使用*showGreeting
方法让视图模型(View Model
)更新,并使用greetingDidChange
*回调函数这样一个简单地属性。
import UIKit
struct Person { //Model
let firstName: String
let lastName: String
}
protocol GreetingViewModelProtocol: class {
var greeting: String? {get}
// function to call when greeting did change
var greetingDidChange: ((GreetingViewModelProtocol) -> ()) ? (get set)
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 = {
[unowned self] 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
}
let model = Person(firstName: "David", lastName: "Blaine")
let viewModel = GreetingViewModel(person: model)
let view = GreetingViewController()
view.viewModel = viewModel
复制代码
再回过来看一下咱们的特色评估:
MVP
的视图拥有更多的职责。由于,前者经过绑定从视图模型(ViewModel
)更新本身,然后者则是把全部的事件前置到Presenter
中,也不对本身的状态进行更新。View Model
对View
一无所知,这可让咱们轻易地对其进行测试。也能够对视图(View
)测试,但因为UIKit
依赖,你可能想跳过她。MVVM
同MVP
有一样的代码量,可是在实际的应用中,对于MVP
你须要把全部事件从视图(View
)前置到展现器(Presenter
)并手动的更新视图,而对于MVVM
,若是你使用了绑定则会变的很容易。MVVM极其吸人眼球,它融合了上述全部架构的的优点,此外,因为它在视图(
View
)端进行了绑定,你能够不须要任何额外的代码对视图(View
)进行更新。虽然如此,可测试性依然保持在一个很好的层次。
###VIPER ####把搭建乐高积木的经验拿到iOS应用设计中使用 VIPER
使咱们最后的选择,这种架构尤其有趣,由于他不是属于MV(X)类的架构。
到目前为止,关于职责粒度的划分很是合理这点你确定赞同。VIPER
在职责划分上面又作了一次迭代,此次咱们一共有五层。
Services
和Managers
,这些不能算是VIPER的一部分,更确切的说只是些外部依赖。Interactor
)中的方法。Interactor
)的职责。VIPER
中的模块。大体上说,VIPER
模块能够是一个界面,也能够是整个应用的用户界面(user story)——想象一下验证功能,它能够是一个界面也能够是几个相关联的界面。”乐高积木“块应该多大呢?——这取决你本身。
若是咱们同MV(X)
这一类进行比较,咱们能够看到几个不一样的职责解耦之处:
Model
)(数据交互(data interaction))逻辑转移到了交互器(Interactor
)中,同时**实体(Entities
)**做为单一的数据结构存在。VIPER
是第一个明确的负责导航功能的架构模式,这点是经过路由(Router
)**来解决的。在iOS应用中,寻一个适当的方式进行页面路由是一个具备挑战性的工做,而MV(X)
这类模式只是简单的避而不谈。
这个例子没有涉及到路由和模块间的交互,由于,这些话题在MV(X)
这类模式中也没有说起。
import UIKIt
struct Person { // 实体(一般要比这个复杂,例如:NSManagedObject)
let firstName: String
let lastName: String
}
struct GreetingData { // 传递数据结构(不是实体)
let greeting: String
let subject: String
}
protocol GreetingOutput: class {
func receiveGreetingData(greetingData: GreetingData)
}
class GreetingInteractor: GreetingProvider {
weak var output: GreetingOutput!
func provideGreetingData() {
let person = Person(firstName: "David", lastName: "Blaine")// 一般来自于数据访问层
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 {
weak var 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
}
let view = GreetingViewController()
let presenter = GreetingPresenter()
let interactor = GreetingInteractor()
view.eventHandler = presenter
presenter.greetingProvider = interactor
interactor.output = presenter
复制代码
再来看一下特色评估:
当使用VIPER
时,感受就像用乐高积木搭建一座帝国大厦同样,这是一个有问题的信号。也许,对于你的应用来讲如今使用VIPER
架构还为时过早,你能够考虑一个简单的架构。有些人则选择忽略这个问题,还继续大炮打麻雀——大材小用。我猜想他们以为将来他们的应用会所以而受益,尽管如今维护成本高的不合情理。若是你也这样想的话,我建议你试一下Generamba——一个能够生成VIPER
架构的工具。尽管如此,对我我的来讲,这样就像在使用有自动瞄准系统的大炮同样而不是简单地投石机。
咱们已经看过了几种架构模式,我但愿你们都能为各自的困惑找到答案,毫无疑问你会意识到这篇文章并无提供什么高招,因此,选择架构模式的关键是根据具体的状况进行权衡、取舍。
所以,在同一个应用中出现架构混合是很正常的一件事。例如:你一开始用的是MVC
架构,忽然你意识到有一个特定的界面很难再用MVC
架构进行有效的维护了,而后你就把它转换成了MVVM
架构并且仅仅只是对这一个界面进行了转换。对于其余的界面若是MVC
架构工做正常的话没有必要进行重构,由于这两个架构很容易兼容。
Make everything as simple as possible, but not simpler——Albert Einstein
尽量的简化一切,但并不简单——阿尔伯特·爱因斯坦