翻译:Clean Swift iOS Architecture for Fixing Massive View Controller (一)

节选翻译来自 Clean Swift iOS Architecture for Fixing Massive View Controller 系列第一篇。
原作者:Raymond
翻译者:Roc Zhang

第一次尝试翻译,如有任何问题还请留言指出。

E-mail: roczhang9673@gmail.com
Weibo: @张鹏roczhang
Twitter: @Lighters9673


开始之前: 原作者提供了付费的 The Clean Swift Handbook,以及可以通过订阅他的 list 来下载 Clean Swift 的 Xcode 模版。示例代码在原文发布后有更新,因此文中所提及代码可能与你下载到的代码有所出入。

Clean Swift - 适用于 iOS 上的 Clean 架构

Clean Swift 架构起源于由 Uncle Bob 提出的 Clean 架构,他们之间有很多概念互通,例如 Components 组件,Boundaries 边界,以及 Models 模型。我将会实现一个由 Uncle Bob 谈过的“创建订单”用例。Uncle Bob 使用 Java 在 Web 应用程序中做了示范,我将会向你展示如何使用 Swift 将 Clean 架构应用到 iOS 项目中。

“创建订单” 用例

这个用例由 Uncle Bob 在 Why can’t anyone get Web architecture right? 中提出。它起源于 Ivar Jacobson 的 Object Oriented Software Engineering 一书。此用例包含了 Clean Swift 中除 routing 外的绝大多数特性,是一个非常好的例子。

Data 数据:
– Customer-id
– Customer-contact-info
– Shipment-destination
– Shipment-mechanism
– Payment-information

Primary Course:
1.用上面的数据命令 clerk 发出“创建订单”命令。
2.系统验证所有数据。
3.系统创建订单并确定订单 ID。
4.系统将订单 ID 传达给 clerk。

Exception Course:
Validation Error 验证错误,由系统将错误信息送达给 clerk。

我们将给这个用例中的数据建模到我们的模型层,并且创建特定的 request(请求)、response(响应)和 view models(视图模型)来在 view controller(视图控制器)、interactor(交互器)和 presenter (展示器)这些组件的边界中传递数据。

首先来看一下我们如何在 Xcode 项目中组织我们的代码。

在 Xcode 中组织你的代码

我们将创建一个新的 Xcode 项目,将其命名为 CleanStore。为了简单起见选择“Single View Application” 和 “仅 iPhone” 就行,并确定在语言中选择了 Swift。接下来,创建一个嵌套的子 Group: Scene -> CreateOrder。当我们稍后开始实现删除订单的用例时,再创建一个新的子 Group 叫做 Scenes -> DeleteOrder

在一个典型的 Xcode 项目中,我们通常会见到文件被组织成 Model(模型)View(视图)Controller(控制器) 三个组。每个 iOS 开发者都知道 MVC 架构,但让并没有告诉你任何关于项目更明确具体的信息。如 Uncle Bob 所指出,group(组)和 file names (文件命名)应当展现出你对此用例的意图,而不应该是反映底层框架结构。所以我们将在 Scene 下嵌套一个新的 group 来管理我们的每一个用例。

CreateOrder 组中,你可以预计到这里所有文件要做的事情都会和创建订单有关。同样的如果在 DeleteOrder 组下你将发现所有的代码都在处理关于删除订单的事务。如果你看到一个由其他开发者创建的新的 ViewOrderHistory 组,你就已经能预计到这是做什么的。
这种结构能够告诉你相比以往使用的 Model, View, Controller 分组来说的更多信息。长此以往,你累计了 15 个 models, 27 个 view controllers,和 17 个 views。它们是做什么的?在一一查看这些文件前,你大概会什么都不清楚。

也许你会问,那些被 CreateOrder, DeleteOrder 以及 ViewOrderHistory 所共用的类和协议该怎么办?实际上你可以将他们放到一个单独的组中,并取名为 Common -> Order。为什么不简单明了一些呢?

回到我们的用例。

CreateOrder group 下面,我们将创建符合 Clean Swift 架构的组件。当我们继续完成用例时,我们将在这些组件的输入输出协议中添加方法,然后实现他们。

VIP 循环

Markdown preferences pane
ViewController(视图控制器),interactor(交互器)和 presenter (展示器)是 Clean Swift 中的三个主要组件。他们彼此互相作为其他组件的输入和输出,正如下面的图表所展示:
视图控制器的输出连接着交互器的输入,交互器的输出连接着展示器的输入,展示器的输出连接着视图控制器的输入。我们将创建特定的对象来在这些组件的边界间传递数据。这能使我们把底层数据结构从组件中解耦。这些特定的对象仅由原始类型组成,如 Int, Double 和 String。我们可以创建结构体、类和枚举来重新表达数据,但在这些实体的内部应该仅包含原始类型。

这一点非常重要。因为一旦业务逻辑改变,底层的数据模型也需要改变。我们不需要去更新全部的代码。Components (组件)在 Clean Swift 中充当插件的角色。这就意味着我们可以将不同的组件交换使用,只要他们能够遵循 Input (输入)和 Output (输出)协议,应用仍然可以按照我们的预期正常工作。

一种典型的情况是这样的:用户点击了应用界面中的一个按钮,Tap Gesture 点击手势通过 IBActions 进入 ViewController 视图控制器,View Controller 构造一个请求对象并将其发送给 Interactor 交互器。Interactor 接受这个请求对象去执行一些任务,然后将执行结果放入 Response 响应对象中并将其发送给 Presenter 展示者。Presenter 将接收 Response 响应对象并格式化结果,然后将格式化的结果放到一个 ViewModel 视图模型对象中,并将其发回给 ViewController。最后,由 ViewController 将结果展示给用户。

1.View Controller

一个 ViewController 视图控制器应当在 iOS App 中承担着怎样的角色?基类 UITableViewController 应当可以给我们一些启示。我们想要使用代码来控制 UITableView 和 UIView 的子类,但这里的的控制代码应当是什么样?怎样的代码能有资格被称作控制代码而怎样的代码不能呢?

让我们深入来看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import UIKit
protocol CreateOrderViewControllerInput
{
func displaySomething(viewModel: CreateOrderViewModel)
}
protocol CreateOrderViewControllerOutput
{
func doSomething(request: CreateOrderRequest)
}
class CreateOrderViewController: UITableViewController, CreateOrderViewControllerInput
{
var output: CreateOrderViewControllerOutput!
var router: CreateOrderRouter!
// MARK: Object lifecycle
override func awakeFromNib()
{
super.awakeFromNib()
CreateOrderConfigurator.sharedInstance.configure(self)
}
// MARK: View lifecycle
override func viewDidLoad()
{
super.viewDidLoad()
doSomethingOnLoad()
}
// MARK: Event handling
func doSomethingOnLoad()
{
// NOTE: Ask the Interactor to do some work
let request = CreateOrderRequest()
output.doSomething(request)
}
// MARK: Display logic
func displaySomething(viewModel: CreateOrderViewModel)
{
// NOTE: Display the result from the Presenter
// nameTextField.text = viewModel.name
}
}

CreateOrderViewControllerInput 和 CreateOrderViewControllerOutput 协议

CreateOrderViewControllerInput 协议指明了 CreateOrderViewController 组件(它遵循这个协议)的输入。CreateOrderViewControllerOutput 协议指明了输出。稍后你将在 interactor 和 presenter 上看到相同的模式。

在 output 协议中有一个方法叫做 doSomething() ,如果有另一个组件想要成为 CreateOrderViewController 的输出,则需要在它的输入中支持 doSomething() 方法。

从之前你看到的 VIP 循环中,我们知道这个 output 是 interactor。但请注意在 CreateOrderViewController.swift 中,没有提及到 CreateOrderInteratcor,这就意味着我们可以将另一个组件替换成为 CreateOrderViewController 的输出,只要这个组件在他的 input 协议中支持 doSomething()
doSomething() 方法中的参数是一个由 view controller 传递到 interactor 的请求对象。这个请求对象是一个 CreateOrderRequest 结构体。它由原始类型构成,而不是之前我们确定的全部的订单数据。这意味着我们可以将底层订单数据模型从 view controller 和 interactor 中解耦出来。所以当我们将来要对订单数据模型做修改的时候(例如在其中增加一个“订单 ID” 的字段),我们不需要去更新 Clean Swift 组件中任何其他的内容。

稍后我们完成了 VIP 循环之后,我们会再回到输出协议中的 displaySomething() 方法。

output 和 router 变量

输出变量是个遵循 CreateOrderViewControllerOutput 协议的对象。虽然我们已经知道在这里它实际上是 interatcor,但也可以不是(因为可以将它替换成其他组件)。

router 变量是一个对 CreateOrderRouter 的引用,它被用来导航到不同的场景。

configure() 方法

我们将在 awakeFromNid() 方法中调用 CreateOrderConfigurator.sharedInstance.configure(self) 去让 configurator 来配置好 VIP 链。在 Uncle Bob 的 Clean 架构或者 VIPER(他将所有的配置工作放在 AppDelegate 中)里没有 configurator (配置器)的存在。由于我真的不想让这些无关的配置代码打乱我们的 VIP 代码,所以我将其提取出来放入到了 configurator 中。稍后我们会来看一下 configurator。

控制流

viewDidLoad() 中,我们需要去运行一些业务逻辑,所以调用 doSomethingOnLoad()。在 doSomethingOnLoad() 中,我们创建一个 CreateOrderRequest() 对象并在 output(the interactor)上调用 doSomething()。就是这样啦。我们要求 output 来执行我们的业务逻辑,view controller 不用也不应该关心谁来做,以及它如何完成。

See you in the next

第一篇先翻译到这里,下次我们再接着 Interactor 继续。