更新:在这可以下载到NSLondon
里面的Sliders
代码
在开发过程中,是否觉得iOS的MVC软件架构很怪异?不知道如何切换到MVVM软件架构?有听说过VIPER架构,但是不知道是否值得一试?
继续往下读,你会找到上述问题的答案,如果找不到 —— 欢迎留言。
本文中,你将了解到关于iOS开发中会用到的软件架构知识。我们会通过理论分析及小练习来评估几个流行的iOS软件架构。如果希望详细了解某些特定知识点,可以点击附带的链接。
掌控软件架构,是会让人上瘾的,因此,请留意:看完本文后,你可能会比看之前提出更多问题,例如:
应该由哪个模块来处理网络请求?Model 还是 Controller?
如何将 Model 的数据「传入」一个 View 的 View Model?
应该由谁来创建新的 VIPER:Router 还是 Presenter?
谁会关心选用哪种软件架构?
如果不使用合适的软件架构,那么终会有一天,你需要调试一个非常庞大,包含了众多业务逻辑的类,那时你就会发现自己根本无从下手。通常来说,开发者无法记住一个庞大的类的所有业务逻辑,因此在分析过程中,往往会因为类的内容过多而忽略掉很多重要的细节。如果你的代码已经遇到这样的问题,那么通常会是这样:
- 这个庞大的类继承了
UIViewController
- 你的数据直接定义在
UIViewController
中 -
UIView
中几乎没有处理任何业务逻辑 - Model 是一个笨重的数据结构
- 单元测试代码覆盖不了任何业务
尽管你完全按照 Apple 的开发指导,并实现了 Apple的MVC架构,上述问题依然会出现。不过,问题并非出在你的身上,而是出在苹果的MVC架构上,后续我们会继续分析这个问题。
我们先来定义什么是好的软件架构:
- 软件架构上具有明确的分工,各个模块的功能职责平衡分配,且明确。
- 良好的可测试性,通常良好的软件架构都具备良好的可测试性。
- 良好的易用性,维护成本低。
为什么需要模块分工?
良好的模块分工,可以大大简化我们对代码的理解难度。虽然通过大量的开发工作,可以训练我们的大脑去分析越来越复杂的逻辑,但是人总有极限,而且简单的逻辑更容易理解、不容易出错,所以,遵循单一职责原则,将复杂的业务逻辑分解。
为什么需要良好的可测试性?
对于深知单元测试好处的开发者来说,这并不是一个问题。单元测试可以大大地减少程序运行时才能发现的问题,这通常可以节省「用户反馈」->「Bug修复」->「新版本发布」->「用户安装新版本」这个耗时长达一周以上的过程。所以,程序的可测试性对于程序的稳定性是异常重要的。
为什么需要良好的易用性?
毋庸置疑,最好的代码是还没被写出来的代码。因此,越少的代码,意味着越少的 bugs。这也意味着尽量以最少的代码实现相同的功能,并非意味着这个开发者懒惰,同时,也不能不看维护成本而盲目赞同一个看似聪明的方案。
MV(X)基础
现今,我们有几种比较流行的软件架构
前三种架构都将app分成三部分:
- Models —— 负责数据处理及数据访问,类似
Person
和PersonDataProvider
类 - Views —— 页面展示层(GUI),在iOS中,所有
ui
前缀的都属于 View - Controller/Presenter/ViewModel —— 在 Model 和 View之间充当通讯媒介,通常负责两方面,一方面当接收到 View 的动作通知时改变 Model,一方面当接收到 Model 的数据变化通知时改变 View 的显示。
将app从架构上分成三部分有利于我们:
- 更好地理解各部分的功能职责,模块间耦合度低
- 更好的复用(通常 View 和 Model 可复用)
- 更好的可测试性,可对每个部分进行独立测试
我们先从 MV(X) 架构开始分析,然后再于 VIPER 进行对比。
MVC
如何使用
在开始讨论 Apple 版本的 MVC 架构前,我们先看看最初的 MVC 架构。
这个框架中,View 并非独立,在 Model 被修改时,View 只是简单地被 Controller 修改。其逻辑与网页更新过程类似:当用户输入网址并回车后,网页被重新加载,并显示远端服务器的内容。虽然我们可以在 iOS 中尝试实现传统 MVC 结构的 App,但由于此架构有一个明显的缺陷 —— 三个部分之间的耦合度非常高,每个部分都必须知道其他部分的具体接口与内容。这大大降低了代码的可重用性 —— 这不是大家希望在程序中使用的方式。因此,我们直接进入下一环节。
传统的 MVC 架构并不适用于现代的 iOS 开发。
Apple 的 MVC
预期效果
上图中可以看出,Controller 在 View 和 Model 中充当着「桥梁」的角色,View 和 Model 相互独立,不需要知道任何对方的细节。虽然 Controller 的复用性很差,不过也可以接受,毕竟很多复杂的业务逻辑是不能放在 Model 里,因此也只能放到 Controller 里的。
理论上整个框架简单明了,不过你是否已经发现一些端倪了?有人说,MVC 可以翻译为 笨重的 View Controller『译者注:原文是 Massive View Controller』。此外,view controller 的瘦身也成了iOS开发者的一大难题。为什么在经过 Apple 改进的 MVC 架构中会出现这样的问题呢?
Apple 的 MVC
实际效果
在Cocoa MVC 中,由于 View 的生命周期,View 和 Controller 基本上绑定在一起,因此开发者也只能编写臃肿的 View Controllers 代码。虽然你已经把一部分业务逻辑和数据修改操作挪到了 Model 层,但如果想对 View 进行瘦身就没那么容易了,大部分时间 View 的职责是向 Controller 发送 action。最终,Controller 会是一个到处都是 delegate,一个臃肿的包含所有变量的 dataSouce,而且通常还需要兼顾异步网络通讯的操作,还有...凡事你想到的,基本都出现会在 Controller 中。
下面的代码是否似曾相似:
var userCell = tableView.dequeneRusableCellWithIdentifier("identifier") as UserCell
userCell.configureWithUser(user)
在此,本应该属于 View 的 cell 直接使用 Model进行配置,换言之, MVC 架构被打破了(MVC 中 View 与 Model 不应该直接通讯),但在iOS开发中,这种情况经常出现,而且开发者不会觉得这样有任何问题。如果开发者在开发过程中严格遵循 MVC 架构,那么他们需要额外设计代码,把 cell 配置挪到 controller 中,避免将 Model 传递到 View 中,这将会导致本来臃肿的 Controller 愈发臃肿。
Cocoa MVC 完全就是 Massive View Controller 的缩写。
这样的架构导致的问题在开发时可能不明显,但一旦到了单元测试阶段(希望你的工程有单元测试),问题将会暴露无遗。由于你工程中的 View controller 和 View 关系紧密,设计测试用例时必须遍历 View 显示时的所有情况,同时需要考虑 View 的生命周期,这使得高覆盖率的测试变得非常困难。
下面我们来看一个运行在 playground 下的例子:『译者注:在遵循原版的基础上,译者对代码进行了少许改善。运行环境「Xcode Version 7.3 (7D175)」』
import UIKit
import XCPlayground
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: #selector(GreetingViewController.didTapButton(_:)), forControlEvents: UIControlEvents.TouchUpInside)
viewLayoutInitial()
}
func didTapButton(button : UIButton!) {
let greeting = "Hello" + " " + self.person.firstName + " " + self.person.lastName
self.greetingLabel.text = greeting
}
func viewLayoutInitial() -> () {
self.view.frame = CGRect(x: 0, y: 0, width: 320, height: 480)
self.view.backgroundColor = UIColor(white: 1.0, alpha: 1.0);
self.showGreetingButton.frame = CGRect(x: 10.0, y: 10.0, width: 90.0, height: 30.0)
self.showGreetingButton.layer.cornerRadius = 6.0
self.showGreetingButton.backgroundColor = UIColor.blueColor()
self.greetingLabel.frame = CGRect(x: 10.0, y: 60.0, width: 200.0, height: 20.0)
self.greetingLabel.textColor = UIColor.blueColor()
self.greetingLabel.text = "Say hello to who?"
self.view.addSubview(self.showGreetingButton);
self.view.addSubview(self.greetingLabel);
}
}
// Assembing of MVC
let model = Person(firstName: "David", lastName: "Blaine")
let view = GreetingViewController()
view.person = model
XCPlaygroundPage.currentPage.liveView = view.view
MVC 架构存在于现在的 view controller 中
上述例子是否不大容易测试?虽然我们可以通过新建 GreetingModel 类并将 greeting 字符串的生成代码放到该类中来实现该部分代码的独立测试,但如果不调用 viewDidLoad
didTapButton
方法,我们很多对 GreetingViewController 中 view 的显示逻辑(虽然上述例子没多少显示逻辑)进行测试。这也意味着在项目单元测试中,我们需要加载所有的 view,这对于单元测试来说是很糟糕的。
实际上,在模拟器(例如 iPhone 4S)上运行所有的 UIViews 并不能保证工程在其他设备(例如 iPad)上能正常运行,所以我建议在 Unit Test target 配置中删除 Host Application,直接对代码进行单元测试。
值得注意的是,View 和 Controller 之间的通讯基本上是不能进行单元测试的。
综上所述,貌似 Cocoa MVC 是一个很差的架构。不过按照文章开头的论述,我们还是从三方面对其进行分析:
- 耦合度 —— View 和 Model 是互相独立的,但是 View 和 View Controller 耦合度很高。
- 可测试性 —— 只有 Model 可以脱离实际运行环境进行单元测试。
- 易用性 —— 相对于其他架构,代码量最少,而且大部分开发者对此架构都很熟悉,易用性良好。
如果你不想花太多时间来选择软件架构,并且觉得稍高的维护工作量会对你的项目造成很大的影响,那么,Cocoa MVC 架构对你来说是一个不错的选择。
MVP
Cocoa MVC 希望成为的架构
是不是看着很像 Apple 的 MVC 架构? 但实际上此架构的名称是MVP(被动类型 View 的变体『译者注:原文为 Passive View variant』)。这是否意味着 Apple 的 MVC 实际上是 MVP ? 并非如此,在 Apple 的 MVC 中,View 和 Controller 是紧密耦合的,但在 MVP 中,Presenter 与 View/View Controller 完全解耦,Presenter中没有任何与 View 布局相关的代码,View 可以很方便地进行移植。即便这样,Presenter 依旧肩负着对 View 的数据更新和动作捕捉。
我要告诉你,UIViewController 实际上就是 View。
在 MVP 架构中,继承了 UIViewController 的子类实际上并非 Presenter ,而是单纯的 View 。这样的分类方式提供了极好的可测试性,与此同时,由于额外实现设计数据和动作之间的绑定,不可避免地会导致开发量的增加。具体例子如下『译者注:在遵循原版的基础上,译者对代码进行了少许改善。运行环境「Xcode Version 7.3 (7D175)」』:
//: Playground - noun: a place where people can play
import UIKit
import XCPlayground
struct Person { // Model
let firstName: String
let lastName: String
}
protocol GreetingView: class {
func setGreeting(greeting: String)
}
protocol GreetingViewPresenter {
init (view : GreetingViewController, person : Person)
func showGreeting()
}
class GreetingPresenter : GreetingViewPresenter { //Presenter
let view : GreetingViewController
var person : Person
required init(view: GreetingViewController, 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 { //View
var presenter : GreetingPresenter!
let showGreetingButton = UIButton()
let greetingLabel = UILabel()
override func viewDidLoad() {
self.showGreetingButton.addTarget(self, action: #selector(GreetingViewController.didTapButton(_:)), forControlEvents: UIControlEvents.TouchUpInside)
viewLayoutInitial()
}
func didTapButton(button : UIButton) {
self.presenter .showGreeting()
}
func setGreeting(greeting: String) {
self.greetingLabel.text = greeting
}
func viewLayoutInitial() {
self.view.frame = CGRect(x: 0, y: 0, width: 320, height: 480)
self.view.backgroundColor = UIColor(white: 1.0, alpha: 1.0);
self.showGreetingButton.frame = CGRect(x: 10.0, y: 10.0, width: 90.0, height: 30.0)
self.showGreetingButton.layer.cornerRadius = 6.0
self.showGreetingButton.backgroundColor = UIColor.blueColor()
self.greetingLabel.frame = CGRect(x: 10.0, y: 60.0, width: 200.0, height: 20.0)
self.greetingLabel.textColor = UIColor.blueColor()
self.greetingLabel.text = "Say hello to who?"
self.view.addSubview(self.showGreetingButton);
self.view.addSubview(self.greetingLabel);
}
}
//Assembling of MVP
let model = Person(firstName: "David", lastName: "Blaine")
let view = GreetingViewController()
let presenter = GreetingPresenter(view: view, person: model)
view.presenter = presenter
XCPlaygroundPage.currentPage.liveView = view.view
关于「聚合」方式的重要说明
由于含有三个完全独立的模块,MVP 是我们讨论的架构中首个暴露出模块聚合问题的架构。虽然我们不希望 View 和 Model 之间有任何直接交互,但在 View 显示时进行模块间的聚合显然是不正确的,尽管我们必须在某个地方实现聚合。例如,我们可以创建一个具有完整App生命周期的「路由服务(Router service)」,专门负责模块间的聚合以及 View 与 View 之间的切换。「聚合」问题会在 MVP 及接下来的架构中一直存在并且不得不解决。
接下来我们分析一下 MVP 架构的特点:
- 耦合度 —— 在此架构中, Presenter 和 Model 模块的职责功能最分明,同时具备一个被尽量简化的 View『译者注:dump的意思为:尽量简化或减少复杂的内容,另其非常容易被理解』
- 可测试性 —— 具备非常好的可测试性,我们可以通过 View 来测试大部分业务逻辑。
- 易用性 —— 对于我们这个没有实用性的简单例子来说,MVP 架构的代码量几乎是 MVC 的两倍,但同时,MVP 在思路上更加清晰。
MVP 架构对于 iOS 开发来说意味着良好的可测试性和大量的代码
MVP
包含「绑定」和「Hooters」『译者注:原文为「With Bindings and Hooters」,此处Hooters用词比较隐晦,未想到合适的翻译方式』
除了上述的 MVP 架构外,还有一种形式的 MVP 架构 —— Supervision Controller MVP. 这个 MVP 变体在 View 和 Model 间建立了直接的「绑定」关系,同时,Presenter(Supervising Controller)依旧负责 View 中 action 的响应以及 View 中数据的更新。
不过,此架构的耦合度比较糟糕,View 和 Model 紧密耦合。这有点类似于 Cocoa 桌面应用开发时遇到的状况。
鉴于此架构的缺陷,在此我们就不举实际代码例子了。
MVVM
最好的 MV(X) 架构,没有之一
MVVM是目前来说最新的 MV(X) 架构,希望它的出现能很好的解决上述架构所面临的问题。
从理论上分析,Model-View-ViewModel 的架构看起来非常完善。其中 View 和 Model 我们已经非常熟悉了, 而 View Model 则相当于两者之间的中间媒介。
MVVM 与 MVP 非常类似:
- 在 MVVM 中,view controller 被当作 view 来处理
- View 和 Model 之间没有直接的联系
除此之外,MVVM 还使用了与 Supervising version MVP 架构类似的「绑定」机制;但是,这个「绑定」并非应用于 View 和 Model 之间,而是应用于 View 和 View Model 之间。
那么在 iOS 中,View Model 实际上是什么呢?从根本上说,View Model 是一个与 UIKit 无关的但负责控制 View 的显示和状态的模块。在运行过程中,View Model 监听着 Model 的变化,并根据 Model 的变化来更新自身对应的变量,同时,由于在 View 和 View Model 间设置了「绑定」,View Model 的变化也会「触发」 View 的更新。
绑定(Bindings)
在 MVP 架构分析的段落中,我们简短地介绍了「绑定」,在此,我们进行更深入的讨论。「绑定」来自于 OS X 开发,但在 iOS 中并没有引入相关的库。虽然在 iOS 中我们有 KVO 和 「通知(notifications)」,但就使用的便捷性来说,「绑定」还是更胜一筹。
鉴于我们不希望重复造轮子,对与「绑定」的应用,我们有下面两种选择:
- 基于 KVO 库设计的 RZDataBinding 和 SwiftBond
- 基于函数式编程设计的库,如ReactiveCocoa, RxSwift和PromiseKit
实际上,如果你有听说过 MVVM —— 你会想到 ReactiveCocoa 和 vice versa. 虽然可以通过简单的「绑定」来实现 MVVM,但 ReactiveCocoa 能帮你更好地实现 MVVM.
不过,关于 reactive 框架,有一个残酷的事实:能力越大,责任越大『译者注:原文为「the great power comes with the great responsibility」,估计是出自漫威「蜘蛛侠」里 Uncle Ben 说的 「with great power comes great responsibility」』。在使用 reactive 框架时,很容易把事情弄得非常复杂。换言之,一个Bug的调试可能会耗费开发者大量的调试时间,看看下面的栈使用情况就能猜到一二了。
杀鸡焉用牛刀,对于我们简单的例子,FRF 和 KVO 都过于复杂,在此,我们可以直接在 ViewModel 中使用 showGreeting
函数和 greetingDidChange
回调函数来对 View 进行更新。例子如下:『译者注:在遵循原版的基础上,译者对代码进行了少许改善。运行环境「Xcode Version 7.3 (7D175)」』
import UIKit
import XCPlayground
struct Person { // Model
let firstName: String
let lastName: String
}
protocol GreetingViewModelProtocol:class {
var greeting:String? { get }
var greetingDidChanged:((GreetingViewModelProtocol) ->())? { get set } //function to call when greeting did change
init(person: Person)
func showGreeting()
}
class GreetingViewModel: GreetingViewModelProtocol {
let person: Person
var greeting: String? {
didSet {
self.greetingDidChanged?(self)
}
}
var greetingDidChanged: ((GreetingViewModelProtocol) -> ())?
required init(person: Person) {
self.person = person
greeting = ""
}
@objc func showGreeting() {
self.greeting = "Hello" + " " + self.person.firstName + " " + self.person.lastName
}
}
class GreetingViewController : UIViewController {
var viewModel: GreetingViewModel! {
didSet {
self.viewModel.greetingDidChanged = { [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: #selector(self.viewModel.showGreeting), forControlEvents: UIControlEvents.TouchUpInside)
viewLayoutInitial()
}
// layout code goes here
func viewLayoutInitial() {
self.view.frame = CGRect(x: 0, y: 0, width: 320, height: 480)
self.view.backgroundColor = UIColor(white: 1.0, alpha: 1.0);
self.showGreetingButton.frame = CGRect(x: 10.0, y: 10.0, width: 90.0, height: 30.0)
self.showGreetingButton.layer.cornerRadius = 6.0
self.showGreetingButton.backgroundColor = UIColor.blueColor()
self.greetingLabel.frame = CGRect(x: 10.0, y: 60.0, width: 200.0, height: 20.0)
self.greetingLabel.textColor = UIColor.blueColor()
self.greetingLabel.text = "Say hello to who?"
self.view.addSubview(self.showGreetingButton);
self.view.addSubview(self.greetingLabel);
}
}
// Assembling of MVVM
let model = Person(firstName: "David", lastName: "Blaine")
let viewModel = GreetingViewModel(person: model)
let view = GreetingViewController()
view.viewModel = viewModel
XCPlaygroundPage.currentPage.liveView = view.view
同样的,我们按照三个标准来对 MVVM 架构进行评判:
- 耦合度 —— 虽然在我们的例子中并不明显,但实际上 MVVM 中的 View 比 MVP 中的 View 具备更多的职责。在 MVVM 中 View 通过与 View Model 间的「绑定」来更新自身,而在 MVP 中,View 只是传递事件给 Presenter ,View 并不更新自身。
- 可测试性 —— 鉴于View Model 对 View 一无所知,我们可以很容易地对 View Model 进行单独测试。虽然 View 同样可以做单元测试,但需要遍历所有页面。
- 易用性 —— 与 MVP 架构相比,MVVM 具备几乎一样的代码量,但在实际项目中,若使用 MVP 架构,你需要传递所有 View 上的事件到 Presenter,同时在 Presenter 中手动更新 View ,而 MVVM 则不需要这样的操作,所以实际使用中 MVVM 会比 MVP 轻巧很多。
MVVM 架构非常诱人,它不仅包含了上述优点,同时由于「绑定」的机制,开发者不需要为更新 View 写额外的代码。除此之外,可测试性也是良好的。
VIPER
乐高建筑的理念移植到 iOS app 架构设计中
VIPER作为我们最后一个候选架构,同时也是最有趣的架构。
VIPER 在任务职责分层上是极好的,为了更好的进行职责分配,VIPER 增加了 Interation 层,至此,VIPER 总共有5个分层。
- Interactor —— 主要负责与数据或网络相关的业务逻辑,例如创建 entity 的实例,从服务器获取数据等。在实现 Interactor 的过程中你可能会使用一些 Service 或 Managers ,这些并不能认为是 VIPER 的一部分,只能说是一些外部依赖。
- Presenter —— 主要负责 UI 相关的业务逻辑,和调用/触发 Interactor 上的接口。
- Entities —— 纯数据对象,不包含对象访问层级,对象访问的逻辑归 Interactor 管理。
- Router —— 主要负责 VIPER 各个模块之间的数据传递工作。
通常来说,VIPER 可以是一个页面,或者整个 app,至于具体怎么设计,完全取决于你。
如果与 MV(X) 的软件架构进行对比,我们会发现职能分配上的一些不同:
- Model(data interaction) 数据处理逻辑转移到了 Interactor 模块,Entities 成为一个纯粹的数据结构。
- 将 Controller/Presenter/ViewModel 中只与 UI 相关的功能转移到 Presenter 中,UI 中的数据处理依然保留在原来的模块中。
- VIPER 是第一个明确定义了负责页面跳转逻辑处理层级的架构,该层级为 Router.
在 iOS中,处理 Router 是一件非常困难的事情,但 MV(X) 架构中不存在这个问题
下面的例子不包含 routing 和 interaction 模块。『译者注:在遵循原版的基础上,译者对代码进行了少许改善。运行环境「Xcode Version 7.3 (7D175)」』
import UIKit
import XCPlayground
struct 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 {
weak var 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 {
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: #selector(GreetingViewController.didTapButton(_:)), forControlEvents: .TouchUpInside)
self.viewLayoutInitial()
}
func didTapButton(button: UIButton) {
self.eventHandler.didTapShowGreetingButton()
}
func setGreeting(greeting: String) {
self.greetingLabel.text = greeting
}
// layout code goes here
func viewLayoutInitial() {
self.view.frame = CGRect(x: 0, y: 0, width: 320, height: 480)
self.view.backgroundColor = UIColor(white: 1.0, alpha: 1.0);
self.showGreetingButton.frame = CGRect(x: 10.0, y: 10.0, width: 90.0, height: 30.0)
self.showGreetingButton.layer.cornerRadius = 6.0
self.showGreetingButton.backgroundColor = UIColor.blueColor()
self.greetingLabel.frame = CGRect(x: 10.0, y: 60.0, width: 200.0, height: 20.0)
self.greetingLabel.textColor = UIColor.blueColor()
self.greetingLabel.text = "Say hello to who?"
self.view.addSubview(self.showGreetingButton);
self.view.addSubview(self.greetingLabel);
}
}
// 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
XCPlaygroundPage.currentPage.liveView = view.view
再次,我们通过三个维度对 VIPER 架构进行分析:
- 耦合度 —— 毫无疑问,VIPER 各模块间的耦合度是最低的。
- 可测试性 —— 同样的,越低的耦合度,越高的可测试性。
- 易用性 —— 尽管功能简单,开发者还是必须额外编写非常多的接口类,开发时间和维护成本很高。
什么是 LEGO ?
在使用 VIPER 时,你可能会觉得自己在用 LEGO 方块拼凑一个帝国大厦,这或许是一个「存在问题」的信号。对于大部分开发者来说,VIPER 显得过于复杂以至于大家很容易就会放弃 VIPER 而寻找更简单的架构。对于一些人来说,他们可能会继续坚持使用 VIPER 架构,尽管这看起来像是在用大炮打麻雀『译者注:原文为「shooting out of cannon into sparrows」』。我觉得他们之所以愿意承受着非常高的维护代价而选择 VIPER,应该是他们觉得日后对他们的 app 会有很大的好处。如果你有相同的想法,不妨试试 Generamba —— 一个自动生成 VIPER 框架的插件。对于我个人来说,这就像使用一个带有全自动目标锁定系统的大炮,而不是一个简易便携的投石器『译者注:原文为「Although for me personally it feels like using an automated targeting system for cannon instead of simply thking a sling shot.」』
结论
在分析了上述几种常用软件框架后,希望你可以为心中的疑问找到答案,但毫无疑问的说,软件世界里没有「尚方宝剑」『译者注:原味为「silver bullet」,典故可参考WIKI』,选择哪种架构,很大程度上取决于你工程的具体情况。
因此,在一个 app 中使用多种软件架构其实是很正常的。例如你的项目开始时使用的是 MVC ,后面你可能发现个别复杂的页面使用 MVC 架构实现时会变得难以维护,此时你可能会使用 MVVM 架构对该界面代码进行重构。但并不需要修改其他使用 MVC 架构的运行良好的页面代码。
事情应该力求简单,不过不能过于简单 —— 爱因斯坦
『译者注:原文为「Everything Should Be Made as Simple as Possible, But Not Simpler —— Albert Einstein」』