iOS 软件架构 - MVC, MVP, MVVM 和 VIPER 「译」


更新:在这可以下载到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架构上,后续我们会继续分析这个问题。

我们先来定义什么是好的软件架构:

  1. 软件架构上具有明确的分工,各个模块的功能职责平衡分配,且明确。
  2. 良好的可测试性,通常良好的软件架构都具备良好的可测试性。
  3. 良好的易用性,维护成本低。

为什么需要模块分工?

良好的模块分工,可以大大简化我们对代码的理解难度。虽然通过大量的开发工作,可以训练我们的大脑去分析越来越复杂的逻辑,但是人总有极限,而且简单的逻辑更容易理解、不容易出错,所以,遵循单一职责原则,将复杂的业务逻辑分解。

为什么需要良好的可测试性?

对于深知单元测试好处的开发者来说,这并不是一个问题。单元测试可以大大地减少程序运行时才能发现的问题,这通常可以节省「用户反馈」->「Bug修复」->「新版本发布」->「用户安装新版本」这个耗时长达一周以上的过程。所以,程序的可测试性对于程序的稳定性是异常重要的。

为什么需要良好的易用性?

毋庸置疑,最好的代码是还没被写出来的代码。因此,越少的代码,意味着越少的 bugs。这也意味着尽量以最少的代码实现相同的功能,并非意味着这个开发者懒惰,同时,也不能不看维护成本而盲目赞同一个看似聪明的方案。

MV(X)基础

现今,我们有几种比较流行的软件架构

前三种架构都将app分成三部分:

  • Models —— 负责数据处理及数据访问,类似 PersonPersonDataProvider
  • 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)」,但就使用的便捷性来说,「绑定」还是更胜一筹。

鉴于我们不希望重复造轮子,对与「绑定」的应用,我们有下面两种选择:

实际上,如果你有听说过 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」』

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容