[译]iOS Architecture Patterns(iOS架构模式)

原文地址

#揭开MVC,MVP,MVVM和VIPER的神秘面纱

Translator-CL

Don’t miss the iOS Developer Roadmap for 2018!

在iOS中用MVC时感觉怎么样? 有关切换到MVVM的疑问? 听说VIPER,但不确定它是否值得?

您即将在iOS环境中构建有关架构模式的知识。 我们将简要回顾一些流行的,并在理论和实践上对它们进行比较,仅举几个小例子。

掌握设计模式可能会让人上瘾,所以要注意:现在,在阅读本文之前,您可能最终会问自己更多问题,例如:

Who supposed to own networking request: a Model or a Controller? 谁应该拥有网络请求:模型或控制器?

How do I pass a Model into a View Model of a new View? 如何将模型传递到新视图的视图模型?

Who creates a new VIPER module: Router or Presenter? 谁创建了一个新的VIPER模块:路由器或演示者?

#Why care about choosing the architecture? 因为如果你不这样做,有一天,用几十种不同的东西调试一个庞大的class,你会发现自己无法找到并修复你class上的任何错误。“ 当然,很难将这个class视为整个实体,因此,你总是会遗漏一些重要的细节。 如果您的应用程序已经处于这种情况,则很可能是:

1、This class is the UIViewController subclass.这个类是UIViewController的子类。

2、Your data stored directly in the UIViewController您的数据直接存储在UIViewController中

3、Your UIViews do almost nothing你的UIViews什么也没做

4、The Model is a dumb data structure模型是一个愚蠢的数据结构

5、Your Unit Tests cover nothing你的单元测试什么也没有

即使您遵循Apple的指导方针并实施Apple的MVC模式,这种情况也可能发生,所以不要感到难过。 Apple的MVC有问题,但我们稍后会再回过头来看看。

让我们定义一个好架构的功能:

1、Balanced distribution of responsibilities among entities with strict roles. 在具有严格作用的实体之间平衡分配责任。

2、Testability usually comes from the first feature (and don’t worry: it is easy with appropriate architecture). 可测试性通常来自第一个功能(不用担心:使用适当的架构很容易)。

3、Ease of use and a low maintenance cost. 易于使用,维护成本低。

#Why Distribution? 为何分配?

在我们试图弄清楚事情是如何运作的时候,分配对我们的大脑负有相当大的负担。 如果你认为你的开发越多,你的大脑就越能适应理解复杂性,那么你就是对的。 但是这种能力不能线性扩展并且很快达到上限。 因此,打败复杂性的最简单方法是按照单一责任原则(Single responsibility principle)在多个实体之间划分责任。

为什么可测性?

对于那些已经对单元测试表示感谢的人来说,这通常不是问题,单元测试在添加新功能后失败或者由于重构了一些复杂的类。 这意味着测试使这些开发人员免于在运行时发现问题,这可能发生在应用程序位于用户设备上并且修复需要一周时间才能到达用户时。

为什么易于使用?

这不需要答案,但值得一提的是,最好的代码是从未编写过的代码。 因此,您拥有的代码越少,您拥有的错误就越少。 这意味着编写更少代码的愿望永远不应仅仅由开发人员的懒惰来解释,而且您不应该倾向于关注维护成本的智能解决方案。

MV(X)要领 现在,在架构设计模式方面,我们有很多选择:

  • MVC

  • MVP

  • MVVM

  • VIPER

前三个假设将应用程序的实体分为以下三类:

Models - 负责域数据或操作数据的数据访问层,思考'Person'或'PersonDataProvider'类。

Views - 负责表示层(GUI),对于iOS环境,可以思考以“UI”前缀开头的所有内容。

Controller / Presenter / ViewModel - 模型和视图之间的粘合剂或介体,通常负责通过响应用户在View上执行的操作以及使用模型更改更新视图来更改模型。

实体划分允许我们:

  • 更好地理解它们(我们已经知道)

  • 重用它们(主要适用于视图和模型)

  • 独立测试它们

让我们从MV(X)模式开始,稍后再回到VIPER。

#MVC

以前是怎样的 在讨论Apple的MVC愿景之前,让我们来看看传统的MVC。

在这种情况下,View是无状态的。 一旦Model改变,它就由Controller简单地渲染。 一旦按下链接导航到其他地方,就可以想象网页完全重新加载。 尽管可以在iOS应用程序中实现传统的MVC,但由于架构问题,它没有多大意义 - 所有三个实体都紧密耦合,每个实体都知道其他两个实体。 这大大降低了每个人的可重用性 - 这不是您希望在应用程序中拥有的内容。 出于这个原因,我们甚至试图编写一个规范的MVC示例。

Traditional MVC doesn't seems to be applicable to modern iOS development.

#Apple’s MVC

Expectation

Controller是View和Model之间的中介,因此他们不了解彼此。 最不可重复使用的是Controller,这通常对我们来说很好,因为我们必须有一个适合所有那些不适合模型的棘手业务逻辑的地方。

从理论上讲,它看起来很简单,但你觉得有些不对劲,对吧? 你甚至听说过人们将MVC简化为大规模视图控制器。 而且,视图控制器卸载成为iOS开发人员的一个重要主题。 Apple刚刚采用传统的MVC并对其进行了一些改进,为什么会发生这种情况呢?

Reality

Cocoa MVC鼓励您编写Massive View控制器,因为它们参与了View的生命周期,很难说它们是分开的。 虽然您仍然可以将一些业务逻辑和数据转换卸载到模型,但是在将工作卸载到View时没有太多选择,在大多数情况下,View的所有责任都是发送操作 到控制器。 视图控制器最终成为所有事物的委托和数据源,并且通常负责调度和取消网络请求,并且......您可以对其进行命名。

你有多少次见过这样的代码:

var userCell = tableView.dequeueReusableCellWithIdentifier("identifier") as UserCell
userCell.configureWithUser(user)
复制代码

这个cell,即直接使用模型配置的View,因此违反了MVC指南,但这种情况一直在发生,通常人们不会觉得它是错误的。 如果您严格遵循MVC,那么您应该从控制器配置单元格,并且不要将模型传递到View中,这将进一步增加Controller的大小。

##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组装

这似乎不太可测试,对吗? 我们可以将问候的生成移动到新的GreetingModel类中并单独测试它,但是我们无法在GreetingViewController内部测试任何表示逻辑(尽管上面的例子中没有太多这样的逻辑)而没有直接调用UIView相关的方法( viewDidLoad,didTapButton)可能会导致加载所有视图,这对单元测试很不利。

实际上,在一个模拟器(例如iPhone 4S)上加载和测试UIViews并不能保证它在其他设备(例如iPad)上正常工作,因此我建议从单元测试目标配置中删除“Host Application” 并在没有应用程序在模拟器上运行的情况下运行测试。

##单元测试不能真正测试视图和控制器之间的交互

尽管如此,Cocoa MVC似乎是一个非常糟糕的选择模式。 但是,让我们根据本文开头定义的功能对其进行评估:

  • Distribution — the View and the Model in fact separated, but the View and the Controller are tightly coupled. 视图和模型实际上是分开的,但视图和控制器是紧密耦合的。

  • Testability — due to the bad distribution you’ll probably only test your Model.由于分布不良,您可能只测试您的模型。

  • Ease of use — the least amount of code among others patterns. In addition everyone is familiar with it, thus, it’s easily maintained even by the unexperienced developers.其他模式中代码量最少。 此外,每个人都熟悉它,因此,即使是没有经验的开发人员也能轻松维护它。

如果您还没准备好在您的架构上投入更多时间,那么Cocoa MVC就是您选择的模式,并且您觉得维护成本较高的东西对于您的小型宠物项目来说太过分了。

总结:就开发速度而言,Cocoa MVC是最好的架构模式。

#MVP

Cocoa MVC’s promises delivered

它看起来不像Apple的MVC吗? 是的,确实如此,它的名字是MVP(Passive View变体)。 但是等一下......这是否意味着Apple的MVC实际上是MVP? 不,它不是,因为如果你还记得那里,View与Controller紧密耦合,而MVP的中介Presenter与视图控制器的生命周期无关,而且View可以很容易地被嘲笑,所以 Presenter中根本没有布局代码,但它负责使用数据和状态更新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
}
// Assembling of MVP
let model = Person(firstName: "David", lastName: "Blaine")
let view = GreetingViewController()
let presenter = GreetingPresenter(view: view, person: model)
view.presenter = presenter
复制代码

##Important note regarding assembly

MVP是第一个揭示由于具有三个实际上分离的层而发生的组装问题的模式。 由于我们不希望View了解Model,因此在呈现视图控制器(即View)中执行汇编是不对的,因此我们必须在其他地方执行。 例如,我们可以制作应用程序范围的Router服务,该服务将负责执行程序集和View-to-View演示。 这个问题出现了,不仅要在MVP中解决,还要在以下所有模式中解决。

让我们来看看MVP的功能:

  • Distribution — we have the most of responsibilities divided between the Presenter and the Model, with the pretty dumb View (in the example above the Model is dumb as well).分布 - 我们在Presenter和模型之间分配了大部分职责,其中哑视图(在模型上面的示例中也是愚蠢的)。

  • Testability — is excellent, we can test most of the business logic due to the dumb View.可测试性 - 非常好,我们可以测试大多数业务逻辑,因为它们是哑视图。

  • Easy of use — in our unrealistically simple example, the amount of code is doubled compared to the MVC, but at the same time, idea of the MVP is very clear. 易于使用 - 在我们不切实际的简单示例中,与MVC相比,代码量增加了一倍,但与此同时,MVP的概念非常清晰。

总结:iOS中的MVP意味着极好的可测试性和大量代码。

#MVP

With Bindings and Hooters

还有MVP的另一种风格 - 监督控制器MVP。 此变体包括View和Model的直接绑定,而Presenter(监督控制器)仍然可以处理来自View的操作,并且能够更改View。

但正如我们之前已经知道的那样,模糊的责任分离是不好的,以及视图和模型的紧密耦合。 这类似于Cocoa桌面开发中的工作方式。

与传统的MVC相同,我没有看到为有缺陷的架构编写示例的重点。

#MVVM

The latest and the greatest of the MV(X) kind

MVVM是最新的MV(X)类型,因此,我们希望它考虑到MV(X)之前面临的问题。

从理论上讲,Model-View-ViewModel看起来非常好。 View和Model已经为我们所熟悉,但也是Mediator,表示为View Model。

它与MVP非常相似:

  • MVVM将视图控制器视为视图

  • View和Model之间没有紧密耦合

此外,它确实像MVP的监督版本一样绑定; 但是,这次不在View和Model之间,而是在View和View Model之间。

那么iOS现实中的View Model是什么? 它基本上是UIKit独立表示您的View及其状态。 视图模型调用模型中的更改并使用更新的模型更新自身,并且由于我们在视图和视图模型之间有绑定,因此第一个更新。

Bindings

我在MVP部分简要提到过它们,但我们在这里讨论它们。 绑定开箱即用于OS X开发,但我们在iOS工具箱中没有它们。 当然我们有KVO和通知,但它们不如绑定方便。

所以,如果我们不想自己编写,我们有两个选择:

事实上,如今,如果你听到“MVVM” - 你认为ReactiveCocoa,反之亦然。 尽管可以使用简单绑定构建MVVM,但ReactiveCocoa(或兄弟姐妹)将允许您获得大部分MVVM。

关于反应性框架有一个痛苦的事实:强大的力量伴随着巨大的责任。 当你被动反应时,很容易弄乱事情。 换句话说,如果你做错了什么,你可能会花很多时间调试应用程序,所以只需看看这个调用堆栈。

在我们的简单示例中,FRF框架甚至KVO都是一种矫枉过正,相反,我们将明确要求View Model使用showGreeting方法进行更新,并使用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
let model = Person(firstName: "David", lastName: "Blaine")
let viewModel = GreetingViewModel(person: model)
let view = GreetingViewController()
view.viewModel = viewModel
复制代码

再次回到我们的功能评估:

  • Distribution — it is not clear in our tiny example, but, in fact, the MVVM’s View has more responsibilities than the MVP’s View. Because the first one updates it’s state from the View Model by setting up bindings, when the second one just forwards all events to the Presenter and doesn’t update itself. 分布 - 在我们的小例子中并不清楚,但事实上,MVVM的View比MVP的View有更多的责任。 因为第一个通过设置绑定从View Model更新它的状态,而第二个只是将所有事件转发给Presenter并且不自行更新。

  • Testability — the View Model knows nothing about the View, this allows us to test it easily. The View might be also tested, but since it is UIKit dependant you might want to skip it. 可测试性 - View Model对View一无所知,这使我们可以轻松地测试它。 View也可能已经过测试,但由于它依赖于UIKit,您可能希望跳过它。

  • Easy of use — its has the same amount of code as the MVP in our example, but in the real app where you’d have to forward all events from the View to the Presenter and to update the View manually, MVVM would be much skinnier if you used bindings. 易于使用 - 它与我们示例中的MVP具有相同数量的代码,但在真实的应用程序中,您必须将所有事件从View转发到Presenter并手动更新View,MVVM将更加出色 如果你使用绑定。

总结:MVVM非常有吸引力,因为它结合了上述方法的优点,此外,由于View端的绑定,它不需要额外的View更新代码。 尽管如此,可测试性仍处于良好水平。

#VIPER

乐高的建筑体验转移到iOS应用程序设计中

VIPER是我们的最后一位候选人,特别有趣,因为它不是来自MV(X)类别。

到目前为止,您必须同意责任的粒度非常好。 VIPER对分离职责的想法进行了另一次迭代,这次我们有五个层次。

  • Interactor — contains business logic related to the data (Entities) or networking, like creating new instances of entities or fetching them from the server. For those purposes you’ll use some Services and Managers which are not considered as a part of VIPER module but rather an external dependency. 交互者 - 包含与数据(实体)或网络相关的业务逻辑,如创建实体的新实例或从服务器获取实体。 出于这些目的,您将使用一些服务和管理器,这些服务和管理器不被视为VIPER模块的一部分,而是外部依赖项。

  • Presenter — contains the UI related (but UIKit independent) business logic, invokes methods on the Interactor.
    Presenter - 包含UI相关(但UIKit独立)业务逻辑,调用Interactor上的方法.

  • Entities — your plain data objects, not the data access layer, because that is a responsibility of the Interactor. 实体 - 您的普通数据对象,而不是数据访问层,因为这是交互者的责任。

  • Router — responsible for the segues between the VIPER modules. 路由器 - 负责VIPER模块之间的隔离。

基本上,VIPER模块可以是一个屏幕或应用程序的整个user story - 想一想身份验证,可以是一个屏幕或几个相关的屏幕。 你的“乐高”积木有多小? - 随你便。

如果我们将它与MV(X)类型进行比较,我们会看到责任分布的一些差异:

  • Model (data interaction) logic shifted into the Interactor with the Entities as dumb data structures. 模型(数据交互)逻辑移入Interactor,实体作为哑数据结构。

  • Only the UI representation duties of the Controller/Presenter/ViewModel moved into the Presenter, but not the data altering capabilities. 只有Controller / Presenter / ViewModel的UI表示职责移入了Presenter,而不是数据更改功能。

  • VIPER is the first pattern which explicitly addresses navigation responsibility, which is supposed to be resolved by the Router. VIPER是第一个明确解决导航责任的模式,它应该由路由器解决。

正确的路由方式对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
interactor.output = presenter
复制代码

再一次,回到功能:

  • Distribution — undoubtedly, VIPER is a champion in distribution of responsibilities. 分配 - 无疑,VIPER是责任分配的倡导者。

  • Testability —no surprises here, better distribution — better testability. 可测试性 - 这里没有惊喜,更好的分布 - 更好的可测试性。

  • Easy of use — finally, two above come in cost of maintainability as you already guessed. You have to write huge amount of interface for classes with very small responsibilities. 易于使用 - 最后,正如您已经猜到的那样,两个以上的可维护性成本。 你必须为责任非常小的类编写大量的接口。

#So what about LEGO?

While using VIPER, you might feel like building The Empire State Building from LEGO blocks, and that is a signal that you have a problem. Maybe, it’s too early to adopt VIPER for your application and you should consider something simpler. Some people ignore this and continue shooting out of cannon into sparrows. I assume they believe that their apps will benefit from VIPER at least in the future, even if now the maintenance cost is unreasonably high. If you believe the same, then I’d recommend you to try Generamba — a tool for generating VIPER skeletons. Although for me personally it feels like using an automated targeting system for cannon instead of simply taking a sling shot. 在使用VIPER时,您可能会感觉要从LEGO块构建帝国大厦,这是一个您遇到问题的信号。 也许,为您的应用程序采用VIPER为时尚早,您应该考虑更简单的事情。 有些人忽略了这一点,继续从大炮射入麻雀。 我认为他们相信他们的应用程序至少在未来会受益于VIPER,即使现在维护成本过高也是如此。 如果你相信,那么我建议你尝试Generamba - 一个生成VIPER骨架的工具。 虽然对我个人来说,感觉就像使用大炮的自动瞄准系统,而不是简单地采取吊索射击。

Conclusion

We went though several architectural patterns, and I hope you have found some answers to what bothered you, but I have no doubt that you realised that there is no silver bullet so choosing architecture pattern is a matter of weighting tradeoffs in your particular situation.

Therefore, it is natural to have a mix of architectures in same app. For example: you’ve started with MVC, then you realised that one particular screen became too hard to maintain efficiently with the MVC and switched to the MVVM, but only for this particular screen. There is no need to refactor other screens for which the MVC actually does work fine, because both of architectures are easily compatible.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值