作者:Bohdan Orlov
原文地址: https://medium.com/ios-os-x-development/ios-architecture-patterns-ecba4c38de52
在iOS开发过程中你是否对MVC的使用感觉很别扭?你是否对转向MVVM有疑惑?你听说过VIPER,但不清楚这个东西是否值得一试。
接着读下去,你会找到上面这些问题的答案。如果读完仍不能解惑,欢迎到评论区捶我。
接下来你将在iOS环境下构建关于架构模式的知识体系。我们将简要构建一些经典的例子,并在理论和实践上进行比较他们的不同。如果你需要更多关于任何一个特定的细节,请关注我。
掌握设计模式容易让人上瘾,所以要注意:阅读本文之前要问自己几个问题:
谁应该持有网络请求:Model还是Controller?
如何在一个新的View中向ViewModel传递Model
谁创建了一个新的VIPER模块:Router还是Presenter
因为如果你不这么做的话,总有一天,等这个类庞大到同时处理十几种事务,你会发现你根本无法从中找到对应代码并修改bug。当然,将这整个类了然于心是很难的,你会常常忘记一些重要的细节。如果你的程序已经处于这种状态了,那它很可能具有下面这些特征:
即使你认为自己遵守了苹果的指导,并按照苹果推荐的MVC设计规范进行开发,但还是遇到了这些问题。不要担心,这是因为苹果的MVC本身就存在一些问题,我们稍后会再来讨论它。
让我们定义一下好的架构应该具备的特点:
1、平衡的分配实体和具体角色的职责
2、把可测试性放在第一位(通过合适的架构,这将很容易实现)
3、易用性和低维护成本
职责的分配能让我们在尝试搞清楚事情如何运作这一过程中保持一个正常的负荷。你可能会认为你投入的精力越多你的大脑越能适应更加复杂的东西,这没错。但是这个能力是非线性的,而且会很快达到临界点。所以降低复杂性的最好的方式是,根据 职责单一原则 将它的功能(职责)分配到多个实体中。
对于那些已经添加了单元测试的项目来说,当他们增加一个新的功能或者重构一个复杂的类时会由单元测试告知失败与否,这多让人很放心啊。同时这也意味着这些测试项将在运行时帮助开发者找到问题,而如果这些问题发生在用户设备上的,解决他们通常会花费一周。
这并不需要答案,但值得一提的是,最好的代码是那些从未写过的代码。所以,代码越少,bug就越少。这意味着,编写更少代码的愿望不应该仅仅由开发人员的懒惰来解释,而且你不应该为了采用更好的解决方案,而对其维护成本视而不见。
如今我们又很多可选的架构方案:
这些实体的分割帮助我们:
让我们开始讲解MV(X)模式,随后是VIPER
在讨论苹果的MVC版本之前,让我们看一下传统的MVC模式:
这个模式下, View 是无状态的。它只是简单的被 Controller 渲染当 Model 变化的时候。想一下Web页面,当你点一个链接尝试跳转时,整个页面都会重新渲染。尽管可以在iOS应用程序中实现传统的MVC,但由于架构问题,这并没有多大意义—— 所有三个实体都是紧密耦合的,每个实体都知道其他两个。这正好降低了他们的重用性,而这又是你在程序中不想看到的。因为这个原因,我们将跳过编写传统MVC代码的示例。
传统的MVC似乎不适合现代的iOS开发
Controller 是 View 和 Model 的中介,因此它俩互相不知道对方。可重用性最差的就是 Controller ,因为我们必须为复杂的业务逻辑提供一个位置, Model 又不适合。
理论上,这看起来很简单,但是你总感觉有什么地方不对,是吧?你甚至听到人们解读MVC为 Massive View Controller 。也因此,视图控制器的简化成了iOS开发一个重要的课题。苹果只是采用传统的MVC并对其进行一些改进,为什么会出现这种情况呢?
Cocoa MVC鼓励你编写大量的视图控制器,因为它们是视图生命周期的一部分,很难说它们是独立的。尽管你有能力转移一些业务逻辑和数据转换工作到 Model 中,当需要转移工作到 View 时你仍然没有太多选择,因为大多数情况 View 的职责就是发送行为到 Controller 。视图控制器最终将成为一个所有东西的委托、数据源、负责调度和取消网络请求,等等。
这种代码,你肯定见过很多少次了:
var userCell = tableView.dequeueReusableCellWithIdentifier("identifier") as UserCell userCell.configureWithUser(user)
这个cell是 View ,直接通过 Model 进行配置,这明显违反了MVC的要求,但这种事情却经常发生,而且认为还不认为这是错的。如果你严格按照MVC的 做法,你应该在Controller里面配置cell,而不是将 Model 直接传递给 View ,但这样就会增加 Controller 的大小。
Cocoa MVC 被称为 Massive View Controller是多么合理啊。
这个问题还不是那么明显,直到提到单元测试(希望它存在于你的项目)。因为你的视图控制器跟View是紧耦合的,这将使得测试非常困难。所以你应该让你的业务逻辑和视图布局代码尽可能分割开来。
让我们看一个简单的例子:
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()
这样根本没法测试,对吧?我们可以把greeting的赋值移到一个新的类GreetingModel中,然后分开测试它。但是我们无法在不直接调用UIView相关方法(viewDidLoad, didTapButton)的情况下测试任何外在的逻辑,而如果这样做,这些方法就导致所有view的刷新,所以这本身就是一个不好的单元测试。
事实上,在一个模拟器上加载和测试UIViews表现正常,不代表它在别的设备依然这样。所以我建议测试时移除单元测试对“宿主程序”的依赖,而直接测试代码。
View和 Controller 之间的交互行为无法通过Unit Tests进行。
根据上面的说法,Cocoa MVC是一个相当不好的架构方案。让我们再来根据文章开头定义的好架构应有的特性来评价下它:
如果你不打算投入很多事情在架构上,或者你感觉对于你们的小项目来说不值得投入过多维护成本,那你应该选择Cocoa MVC。
Cocoa MVC 是开发速度最快的一种架构。
这是不是更苹果的MVC非常像?确实是这样的,它的名字叫做 MVP 。等一下,这是不是意味着苹果的MVC事实上就是MVP?不。你可以再观察下这个结构, View 和 Controller 是紧耦合关系,作为MVP的中介者 – Presenter 并没有管理视图控制器的生命周期,它里面也不含有布局代码,它的职责是根据数据的状态变化更新 View ,所以呢, View 这一层就可以很简单的抽出来。
我会告诉你,UIViewController也是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: 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 }
MVP是第一个揭露三层模型装配问题的模式。我们不想让 View 和 Model 互通,因为在试图控制器(View)中执行装配操作是明显不对的,所以我们只能换个地方放装配的代码。例如,我们可以做一个应用范围的 Router 服务,它负责装配工作和 View 到 View 的展示。这个问题的出现不仅要在MVP中解决,在以下的几个模式中也都要解决。
我们看下MVP的特性:
在iOS中MVP模式意味着良好的可测试性和大量代码。
这是另一个MVP的样式 – 由视同控制器担当管理的MVP。这个变体中, View 和 Model 是直接绑定的, Presenter (担当管理的控制器)仍然处理着来自 View 的操作,并且能够改变 View 。
但是通过上面的学习我们已经知道了,将 View 和 Model 紧耦合处理,这种不明确的职责分离是很糟糕的。这与Cocoa桌面开发中的工作原理类似。
跟传统MVC一样,我找不到要为这个有缺陷的架构写示例的理由。
MVVM 是最新的MV(X)类型,希望它能解决我们之前讨论过的问题。
MVVM理论上看起来是很好的, View 和 Model 我们已经很熟悉了,它俩之间的中介者由 View Model 表示。
这和MVP很像:
此外它的绑定逻辑很像MVP的监管版本;但是这次不是 View 和 Model ,而是 View 和 View Model 之间的绑定。
所以iOS当中的 View Model 到底是什么呢?它是UIKit独立于视图及其状态的表示。 View Model 调用 Model 执行更改,然后根据 Model 的更新再更新自己,因为我们绑定了 View 和 View Model ,第一个模型将相应的更新。
我在MVP部分明确提到过绑定,这次让我们再来讨论一下。绑定出自于MacOS开发,在iOS中是没有的。我们虽然可以通过KVO和通知完成这一过程,但是这样的绑定方式并不方便。
如果我们不想自己实现的话,有两个选项可供参考:
如今当你听到“MVVM”,就应该想到ReactiveCocoa。因为它可以让你用很简单的绑定方式构建MVVM,几乎涵盖所有MVVM中的逻辑。
但是使用响应式框架会面临一个不好的现实:能力越大责任越大。使用reactive很容易将事情复杂化。也就是说,如果发生了一处错误,你需要花费很多时间去调试问题,可以简单看下响应式的调用堆栈。
在我们的示例中,响应式框架甚至KVO都是多余的,我们将使用showGreeting方法显式地要求 View Model 更新,并使用greetingDidChange回调函数的简单属性。
import UIKit struct Person { // Model let firstName: String let lastName: String } protocol GreetingViewModelProtocol: class { var greeting: String? { get } var greetingDidChange: ((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.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 } // Assembling of MVVM
再次回来用这三个特征验证一下:
MVVM是很有吸引力的,因为它包含了前面提到的优点,此外通过View层的绑定,也不需要额外的代码处理View更新。测试性也还不错。
VIPER是我们最后一个候选模式,有趣的一点是它不属于MV(X)类型。
目前为止,你必须同意职责的粒度是很重要的。VIPER在划分职责层面又做了一次迭代,它将项目划分成5层。
基本上,VIPER模块可以是一整屏内容,也可以是你应用中完整的用户行为 – 想一下授权行为,它可以在一个或者几个相关联的界面。“乐高”方块应该多小呢?这取决于你。
如果我们将它和MV(X)类比,会发现一些在职责划分上的区别:
在iOS应用中用一个优雅的方式处理跳转问题确实是一个挑战,MV(X)模式没有处理这个问题。
该示例不涉及模块之间的 路由 或 交互 ,因为MV(X)模式根本不涉及这些主题。
import UIKit 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: "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
让我们再一次对比那几个特征:
当使用VIPER时,如果你感觉就像是通过乐高方块搭建帝国大厦,这就意味着出现了问题。不应该过早在你的应用中使用VIPER,你需要考虑简便性。有些人不注意简便性,直接使用VIPER,会有点大材小用。我猜测很多人是这么想的,他们的应用迟早都会发展到适用VIPER的复杂程度,所以早晚都会做的事,即使现在维护成本高也应该继续做下去。如果你就是这么想的,我推荐你试一下 Generamba – 一个生成VIPER组件的工具。虽然对我个人来说,这感觉就像使用自动瞄准系统而不是简单的弹射。
我们已经讲解了几个架构模式,希望你能解答曾经困扰你的问题。我敢肯定你也意识到了架构模式的选择没有最好这一说,它取决于你在特定环境下权衡利弊之后做的选择。
所以,在一个应用中出现混合一种混合的架构模式也是很常见的。例如,你一开始使用MVC,然后你发现有一个界面的逻辑变得很复杂,然后你转向了MVVM,但也是仅限于这个界面。你不必重构别的使用MVC的界面,因为它原本就是工作的好好的,而且这两个架构模式是很容易兼容的。
事情应该力求简单,不过不能过于简单 – 阿尔伯特·爱因斯坦
我来评几句
登录后评论已发表评论数()