【译】A taste of MVVM and Reactive paradigm

【译】A taste of MVVM and Reactive paradigm

Medium 原文 A taste of MVVM and Reactive paradigm
原文博客 A taste of MVVM and Reactive paradigm

我喜欢 Swift,就像许多其他面向对象的编程语言一样。 Swift 允许你表示具有某些特点和执行一些操作的真实世界对象。

我倾向于认为 App 是一个每个对象都是一个人的世界。他们工作和沟通。如果一个人不能独自完成工作,他需要寻求帮助。举一个项目,例如,如果经理必须自己完成所有的工作,他会发疯的。因此需要组织和委派任务,并且需要许多人在项目上进行协作:设计师,测试人员,Scrum 主管,开发人员。任务完成后,需要通知经理。

这可能不是一个好例子。但至少你了解 OOP 中沟通和授权的重要性。当我开始 iOS 编程时,我对“架构”一词非常感兴趣。但在做了一段时间后,这一切都归结为识别和分担责任。本文讲述了 MVC 和 MVVM 的简单 Extract 类重构,以及如何进一步研究 Rx。您可以自由地创建自己的架构,但无论您做什么,一致性都是关键,不要让您的队友感到困惑或惊讶。

MVC

看看你最熟悉的架构 - MVC,模型视图控制器的简称。 在新建一个 iOS 项目时总是会得到一个这样的架构。 View 是您使用 UIViewUIButtonUILabel 呈现数据的位置。 Model 只是数据的一个设想的词。 它可以是您的实体,来自网络的数据,来自数据库的对象或来自缓存。Controller 是在 Model 和 View 间进行调解的东西。

宇宙中心 - UIViewController

ViewController 的问题在于它往往是巨大的。 Apple 把它作为宇宙的中心,它拥有许多属性和责任。你可以用 UIViewController 做很多事情。诸如与故事板交互,管理视图,配置视图轮换,状态恢复等事情。 UIViewController 设计了很多可以覆盖和自定义的方法。

看看 UIViewController 文档 中的许多部分,如果没有 UIViewController,则无法执行以下操作。

1
2
3
4
5
6
7
8
9
10
11
func viewDidLoad()
var preferredStatusBarStyle: UIStatusBarStyle { get }
UITableViewDataSource
var presentationController: UIPresentationController? { get }
func childViewControllerForScreenEdgesDeferringSystemGestures() -> UIViewController?
func didMove(toParentViewController parent: UIViewController?)
var systemMinimumLayoutMargins: NSDirectionalEdgeInsets
var edgesForExtendedLayout: UIRectEdge
var previewActionItems: [UIPreviewActionItem]
var navigationItem: UINavigationItem
var shouldAutorotate: Bool

随着应用程序的增长,我们需要为其他逻辑添加更多代码。例如网络,数据源,处理多个代理,present 或 push 子视图控制器。当然,我们可以将所有内容放在视图控制器上,但这会产生出一个超大的 viewController.m 文件,这是很容易让你失去对 viewController 的把控,因为所有的东西都放在了这个巨型视图控制器中。你会倾向于引入重复的代码,并且修复错误变得很难,因为它们遍布各处。

Windows Phone 中的 Page 或 Android 中的 Activity 也是如此。它们用于屏幕或部分功能屏幕。某些操作只能通过它们完成,如 Page.OnNavigatedToActivity.onCreate

架构术语

当 ViewController 做很多事情时你会怎么做?您将工作移到其他组件。顺便说一句,如果您希望其他对象执行用户输入处理,则可以使用 Presenter。如果 Presenter 做得太多,那么它可以将业务逻辑偏移到 Interactor。此外,还有更多架构术语可供使用。

1
2
3
4
5
6
let buzzWords = [
"Model", "View", "Controller", "Entity", "Router", "Clean", "Reactive",
"Presenter", "Interactor", "Megatron", "Coordinator", "Flow", "Manager"
]
let architecture = buzzWords.shuffled().takeRandom()
let acronym = architecture.makeAcronym()

在所有架构术语汇编完成后,我们得到了一个架构。这里有更多,包括简单的提取类重构,拥抱 MVC 或从 Clean Code,Rx,EventBus 或 Redux 中获取灵感。选择取决于项目,有些团队更喜欢这类架构而不是另一种架构。

务实的程序员

人们对什么是好的架构有不同的看法。对我来说,这是关于明确的关注分离,良好的沟通模式和使用舒适度。架构中的每个组件都应该是可识别的并且具有特定的角色。沟通必须清楚,以便我们知道哪个对象正在相互通信。这与良好的依赖注入一起使测试更容易。

理论上听起来不错的事情在实践中可能效果不佳。分离的域对象很酷,协议扩展很酷,多层抽象很酷。但是它们中可能有太多的问题。

如果你对设计模式有足够的了解,你就知道它们都归结为这些简单的原则:

  • 封装变化的内容:确定应用程序的各个方面的变化,并将它们与保持不变的方面分开。
  • 编程到接口,而不是实现
  • 更喜欢继承的组合

如果我们要掌握一件事,那就是画结构图。关键是要确定责任,并以合理和一致的方式将其组成。向你的队友咨询最合适的,总是在编写代码的时候考虑到你也将是未来的维护者。然后你就会写得更好。

不要和系统做斗争

一些架构引入了全新的范例。其中有些很麻烦,人们编写脚本来生成模板代码。有很多解决问题的方法是好的。但对我来说,有时候我觉得他们在与这个体系作斗争。有些任务很容易,而有些琐碎的任务则变得非常困难。我们不应该仅仅因为一个架构是时髦的,就把自己限制在一个架构中。要务实,不要武断。

在 iOS 中,我们应该接受 MVC。UIViewController 不适用于内容的全屏显示。它们可以拆分和组合达到拆分功能的目的。我们可以使用 CoordinatorFlowController 来管理依赖关系和处理流。状态转换容器,嵌入式逻辑控制器内容切分。这种令人欣慰的 ViewController 方法在 iOS 中可以很好地与 MVC 配合使用,是我的首选方法。

MVVM

另一个足够好的方法是将一些任务重定向到另一个对象,我们称之为 ViewModel 。这个名字不重要,你可以把它命名为反应堆,大师,恐龙。重要的是你的团队要有一个约定的名字。ViewModel 从 ViewController 中拆分一些任务,并在完成后告诉 ViewController。CocoaTouch 中有一些通信模式,例如要使用的委托、闭包。

ViewModel 是独立的,没有对 UIKit 的引用,只有输入和输出。我们可以把很多东西放到 ViewModel 中,比如计算、格式化、联网、业务逻辑。此外,如果您不喜欢 ViewModel 变得庞大,那么您肯定需要创建一些专用的对象。ViewModel 是获得超薄 ViewController 的第一步。

同步

下面是一个非常简单的视图模型,它基于用户模型格式化数据,是同步进行的。

1
2
3
4
5
6
7
8
9
10
class ProfileController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let viewModel = ViewModel(user: user)
nameLabel.text = viewModel.name
birthdayLabel.text = viewModel.birthdayString
salaryLabel.text = viewModel.salary
piLabel.text = viewModel.millionthDigitOfPi
}
}

异步

我们一直在使用异步 API。如果我们想显示用户的 Facebook 好友数量呢?为了实现这一点,我们需要调用 Facebook API,而这个操作需要时间。视图模型可以通过闭包进行报告。

1
2
3
viewModel.getFacebookFriends { friends in
self.friendCountLabel.text = "\(friends.count)"
}

在内部,ViewModel 可以将任务重定向到专用的 Facebook API 客户端对象.

1
2
3
4
5
6
7
8
9
10
class ViewModel {
func getFacebookFriends(completion: [User] -> Void) {
let client = APIClient()
client.getFacebookFriends(for: user) { friends in
DispatchQueue.main.async {
completion(friends)
}
}
}
}

Android版Jetpack

谷歌在 2017 年的谷歌 IO 上推出了 Android 架构组件,现在是 Jetpack 的一部分。它有 ViewModel 和 LiveData ,这也是一种应用于 Android 的 MVVM 。ViewModel 通过配置更改存活下来,并根据要使用的活动的 LiveData 通知结果。

1
2
3
4
5
6
7
8
9
10
class MyActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
super.onCreate(savedInstanceState, persistentState)

val model = ViewModelProviders.of(this).get(MyViewModel::class.java)
model.getUsers().observe(this, { users ->
// update UI
})
}
}

这就是我喜欢 ViewModel 的原因之一。如果我们遵循这样的 ViewModel ,那么 iOS 和 Android 之间的代码结构就会变得相似。不需要一些随机的 JavaScript 跨平台解决方案。您只需学习一次这个概念,就可以将其应用到 iOS 和 Android 上。我在 iOS 上学习 ViewModel、RxSwift ,当我在 Android 上使用 RxJava 和 RxBinding 时,感觉就像在家一样。Kickstarter 项目也证明了这在 iOS 和 Android 应用程序中很好地工作。

绑定

为了封装闭包,我们可以创建一个名为 Binding 的类,它可以通知一个或多个监听器。它利用了 Didset 的优点,使其可观测性变得清晰。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Binding<T> {
var value: T {
didSet {
listener?(value)
}
}
private var listener: ((T) -> Void)?
init(value: T) {
self.value = value
}
func bind(_ closure: @escaping (T) -> Void) {
closure(value)
listener = closure
}
}

以下是如何在 ViewModel 中使用的 Binding 示例:

1
2
3
4
5
6
7
8
9
10
11
class ViewModel {
let friends = Binding<[User]>(value: [])
init() {
getFacebookFriends {
friends.value = $0
}
}
func getFacebookFriends(completion: ([User]) -> Void) {
// Do the work
}
}

不论何时,当获取或更改 friends 时,ViewController 会相应地更新。这叫做对变化的反应。
你经常看到 MVVM 引入了反应式框架,这是有原因的。它们提供了许多链接操作符,并使反应式编程更容易和更具声明性。

RxSwift

也许 Swift 中最常见的反应式框架是 RXSwift。我喜欢它的一点是它遵循了响应式编程模式。因此,如果您已经使用了 RxJava 、RxJS 或 RxKotlin ,您会感到更加熟悉。

Observable


RXSwift 通过 Observable 统一了同步和异步操作。你应该像下面这么做。

1
2
3
4
5
6
7
8
9
10
11
12
13
class ViewModel {
let friends: Observable<[User]>
init() {
let client = APIClient()
friends = Observable<[User]>.create({ subscriber in
client.getFacebookFriends(completion: { friends in
subscriber.onNext(friends)
subscriber.onCompleted()
})
return Disposables.create()
})
}
}

RXSwift 的强大功能在于它的众多操作符,这些操作符可以帮助您链接可观察的对象。在这里,您可以调用 2 个网络请求,等待两个请求都完成,然后汇总 friends。这是非常流线型的,可以节省你很多时间。您可以在这里注册 Observable 监听,当请求完成时会触发它:

1
2
3
4
5
6
override func viewDidLoad() {
super.viewDidLoad()
viewModel.friends.subscribe(onNext: { friends in
self.friendsCountLabel.text = "\(friends.count)"
})
}

输入和输出

ViewModel 和 RX 的一个优点是,我们可以使用 Observable 分离输入和输出,它提供了一个清晰的界面。点击阅读更多源码内容: Input and output container

下面很明显, fetch 是一个输入,而 friends 是可行的输出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class ViewModel {
class Input {
let fetch = PublishSubject<()>()
}
class Output {
let friends: Driver<[User]>
}
let apiClient: APIClient
let input: Input
let output: Output
init(apiClient: APIClient) {
self.apiClient = apiClient
// Connect input and output
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class ProfileViewController: BaseViewController<ProfileView> {
let viewModel: ProfileViewModelType
init(viewModel: ProfileViewModelType) {
self.viewModel = viewModel
}
override func viewDidLoad() {
super.viewDidLoad()
// Input
viewModel.input.fetch.onNext(())
// Output
viewModel.output.friends.subscribe(onNext: { friends in
self.friendsCountLabel.text = "\(friends.count)"
})
}
}

reactive 如何工作

如果你喜欢 Rx ,在使用一些框架一段时间后,了解它们是很好的。有一些概念,如 Signal, SignalProducer, Observable, Promise, Future, Task, Job, Launcher, Async,有些人对它们可以有很好的区分。在这里,我简单地称之为 Signal,它是一种可以发出信号值的东西。

Monad

Signal 及其 Result 只是 Monads ,它是可以被映射和链接的东西。

Signal 使用延迟的执行回调闭包。它可以获取或推送。这就是 Signal 更新值和调用回调的顺序的方式。

执行回调方法意味着我们将一个函数传递给另一个函数。传入函数在适当的时候被调用。

同步和异步

Monad 可以是同步模式或异步模式。同步更容易理解,但异步在实践中已经很熟悉和使用了。

  • 同步:通过返回立即得到返回值
  • 异步:通过回调块得到返回值

下面是一个简单的同步和异步自由函数示例:

1
2
3
4
5
6
7
8
9
10
11
12
// Sync
func sum(a: Int, b: Int) -> Int {
return a + b
}

// Async
func sum(a: Int, b: Int, completion: Int -> Void) {
// Assumed it is a very long task to get the result
let result = a + b

completion(result)
}

以及同步和异步如何应用于返回值类型。注意异步版本,我们在一个完成闭包中得到转换值,而不是从函数立即返回。

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
enum Result<T> {
case value(value: T)
case failure(error: Error)

// Sync
public func map<U>(f: (T) -> U) -> Result<U> {
switch self {
case let .value(value):
return .value(value: f(value))
case let .failure(error):
return .failure(error: error)
}
}

// Async
public func map<U>(f: @escaping ((T), (U) -> Void) -> Void) -> (((Result<U>) -> Void) -> Void) {
return { g in // g: Result<U> -> Void
switch self {
case let .value(value):
f(value) { transformedValue in // transformedValue: U
g(.value(value: transformedValue))
}
case let .failure(error):
g(.failure(error: error))
}
}
}
}

推送信号

给出这样一个信号链:
A -(map)-> B -(flatMap)-> C -(flatMap)-> D -(subscribe)
推送信号,当 信号A 在一个事件发生时,它通过 CallBacks 事件传播。PushSignal 在 RxSwift 中类似于 PublishSubject

  • 通过向源信号发送事件触发。
  • 我们必须保持 A,因为它使其信号保持
  • 我们订阅最后一个 D
  • 我们将事件发送到第一个 A
  • A 的回调被调用,它依次使用 A 的映射结果调用 B 的回调,然后 B 的回调使用 B 的平面映射结果调用 C 的回调,依此类推。

它类似于 Promise A+ ,您可以在我的 Then framework 中看到 Promise A+ 的 Swift 实现。现在,这里是一个简单的 PushSignal 的 Swift 4 实现。

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
public final class PushSignal<T> {
var event: Result<T>?
var callbacks: [(Result<T>) -> Void] = []
let lockQueue = DispatchQueue(label: "Serial Queue")

func notify() {
guard let event = event else {
return
}

callbacks.forEach { callback in
callback(event)
}
}

func update(event: Result<T>) {
lockQueue.sync {
self.event = event
}

notify()
}

public func subscribe(f: @escaping (Result<T>) -> Void) -> Signal<T> {
// Callback
if let event = event {
f(event)
}

callbacks.append(f)

return self
}

public func map<U>(f: @escaping (T) -> U) -> Signal<U> {
let signal = Signal<U>()

_ = subscribe { event in
signal.update(event: event.map(f: f))
}

return signal
}
}

下面是如何使用 PushSignal 将链从字符串转换为其长度,您应该看到 4,即打印的字符串 “test” 的长度。

1
2
3
4
5
6
7
8
9
10
11
12
13
let signal = PushSignal<String>()

_ = signal.map { value in
return value.count
}.subscribe { event in
if case let .value(value) = event {
print(value)
} else {
print("error")
}
}

signal.update(event: .value(value: "test"))

获取信号

给出这样一个信号链:
A -(map)-> B -(flatMap)-> C -(flatMap)-> D -(subscribe)

获取信号,有时称为 Future,意味着当我们订阅最终的信号 D 时,它会导致先前的信号被激活:

  • 通过订阅最终信号 D 触发;
  • 我们必须保持 D,因为它使其信号保持
  • 我们订阅最后一个 D
  • D 的操作运行,它导致 C 的操作运行,… 然后 A 的操作运行。执行任务(如获取网络、检索数据库、文件访问、大量计算等)以获取结果,并调用A的完成。然后,A 的完成调用 B 的完成,结果由 B 的映射映射,…一直映射到订阅方的完成 block。

这里是 PullSignal 的一个 Swift 4 实现。PullSignal 类似于 Rxswift 中的 Observable 和 ReactiveSwift 中的SignalProducer。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public struct PullSignal<T> {
let operation: ((Result<T>) -> Void) -> Void

public init(operation: @escaping ((Result<T>) -> Void) -> Void) {
self.operation = operation
}

public func start(completion: (Result<T>) -> Void) {
operation() { event in
completion(event)
}
}

public func map<U>(f: @escaping (T) -> U) -> PullSignal<U> {
return PullSignal<U> { completion in
self.start { event in
completion(event.map(f: f))
}
}
}
}

链是不活动的,直到您调用链中的最后一个信号开始,这将触发操作流到第一个信号。运行这个代码,您应该看到 4 ,控制台上打印的字符串 “test” 的长度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let signal = PullSignal<String> { completion in
// There should be some long running operation here
completion(Result.value(value: "test"))
}

signal.map { value in
value.count
}.start { event in
if case let .value(value) = event {
print(value)
} else {
print("error")
}
}

我希望这些代码段足够简单,能够帮助您理解信号在后台是如何工作的,以及如何区分冷热信号。为了得到一个完全工作的信号框架,您需要实现更多的操作。如 retry , rebounce , throttle , queue , flatten, filter, delay, combine 和添加 UIKit 支持,就像 RxCocoa 所做的,具体可以在我的 Signal repo 中查看实现。

总结

架构是一个非常常见的话题。希望这篇文章能给您的决策带来一些想法。MVC 在 iOS 中占主导地位,MVVM 是一个好朋友,RX 是一个强大的工具。以下是一些更有趣的读物: