【译】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 是您使用 UIView
,UIButton
,UILabel
呈现数据的位置。 Model 只是数据的一个设想的词。 它可以是您的实体,来自网络的数据,来自数据库的对象或来自缓存。Controller 是在 Model 和 View 间进行调解的东西。
宇宙中心 - UIViewController
ViewController
的问题在于它往往是巨大的。 Apple 把它作为宇宙的中心,它拥有许多属性和责任。你可以用 UIViewController
做很多事情。诸如与故事板交互,管理视图,配置视图轮换,状态恢复等事情。 UIViewController
设计了很多可以覆盖和自定义的方法。
看看 UIViewController
文档 中的许多部分,如果没有 UIViewController
,则无法执行以下操作。
1 | func viewDidLoad() |
随着应用程序的增长,我们需要为其他逻辑添加更多代码。例如网络,数据源,处理多个代理,present 或 push 子视图控制器。当然,我们可以将所有内容放在视图控制器上,但这会产生出一个超大的 viewController.m 文件,这是很容易让你失去对 viewController 的把控,因为所有的东西都放在了这个巨型视图控制器中。你会倾向于引入重复的代码,并且修复错误变得很难,因为它们遍布各处。
Windows Phone 中的 Page
或 Android 中的 Activity
也是如此。它们用于屏幕或部分功能屏幕。某些操作只能通过它们完成,如 Page.OnNavigatedTo ,Activity.onCreate 。
架构术语
当 ViewController 做很多事情时你会怎么做?您将工作移到其他组件。顺便说一句,如果您希望其他对象执行用户输入处理,则可以使用 Presenter。如果 Presenter 做得太多,那么它可以将业务逻辑偏移到 Interactor。此外,还有更多架构术语可供使用。
1 | let buzzWords = [ |
在所有架构术语汇编完成后,我们得到了一个架构。这里有更多,包括简单的提取类重构,拥抱 MVC 或从 Clean Code,Rx,EventBus 或 Redux 中获取灵感。选择取决于项目,有些团队更喜欢这类架构而不是另一种架构。
务实的程序员
人们对什么是好的架构有不同的看法。对我来说,这是关于明确的关注分离,良好的沟通模式和使用舒适度。架构中的每个组件都应该是可识别的并且具有特定的角色。沟通必须清楚,以便我们知道哪个对象正在相互通信。这与良好的依赖注入一起使测试更容易。
理论上听起来不错的事情在实践中可能效果不佳。分离的域对象很酷,协议扩展很酷,多层抽象很酷。但是它们中可能有太多的问题。
如果你对设计模式有足够的了解,你就知道它们都归结为这些简单的原则:
- 封装变化的内容:确定应用程序的各个方面的变化,并将它们与保持不变的方面分开。
- 编程到接口,而不是实现
- 更喜欢继承的组合
如果我们要掌握一件事,那就是画结构图。关键是要确定责任,并以合理和一致的方式将其组成。向你的队友咨询最合适的,总是在编写代码的时候考虑到你也将是未来的维护者。然后你就会写得更好。
不要和系统做斗争
一些架构引入了全新的范例。其中有些很麻烦,人们编写脚本来生成模板代码。有很多解决问题的方法是好的。但对我来说,有时候我觉得他们在与这个体系作斗争。有些任务很容易,而有些琐碎的任务则变得非常困难。我们不应该仅仅因为一个架构是时髦的,就把自己限制在一个架构中。要务实,不要武断。
在 iOS 中,我们应该接受 MVC。UIViewController
不适用于内容的全屏显示。它们可以拆分和组合达到拆分功能的目的。我们可以使用 Coordinator 和 FlowController 来管理依赖关系和处理流。状态转换容器,嵌入式逻辑控制器,内容切分。这种令人欣慰的 ViewController
方法在 iOS 中可以很好地与 MVC 配合使用,是我的首选方法。
MVVM
另一个足够好的方法是将一些任务重定向到另一个对象,我们称之为 ViewModel 。这个名字不重要,你可以把它命名为反应堆,大师,恐龙。重要的是你的团队要有一个约定的名字。ViewModel 从 ViewController 中拆分一些任务,并在完成后告诉 ViewController。CocoaTouch 中有一些通信模式,例如要使用的委托、闭包。
ViewModel 是独立的,没有对 UIKit 的引用,只有输入和输出。我们可以把很多东西放到 ViewModel 中,比如计算、格式化、联网、业务逻辑。此外,如果您不喜欢 ViewModel 变得庞大,那么您肯定需要创建一些专用的对象。ViewModel 是获得超薄 ViewController 的第一步。
同步
下面是一个非常简单的视图模型,它基于用户
模型格式化数据,是同步进行的。1
2
3
4
5
6
7
8
9
10class 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
3viewModel.getFacebookFriends { friends in
self.friendCountLabel.text = "\(friends.count)"
}
在内部,ViewModel 可以将任务重定向到专用的 Facebook API 客户端对象.
1 | class ViewModel { |
Android版Jetpack
谷歌在 2017 年的谷歌 IO 上推出了 Android 架构组件,现在是 Jetpack 的一部分。它有 ViewModel 和 LiveData ,这也是一种应用于 Android 的 MVVM 。ViewModel 通过配置更改存活下来,并根据要使用的活动的 LiveData 通知结果。1
2
3
4
5
6
7
8
9
10class 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 | class Binding<T> { |
以下是如何在 ViewModel 中使用的 Binding 示例:
1 | class ViewModel { |
不论何时,当获取或更改 friends 时,ViewController 会相应地更新。这叫做对变化的反应。
你经常看到 MVVM 引入了反应式框架,这是有原因的。它们提供了许多链接操作符,并使反应式编程更容易和更具声明性。
RxSwift
也许 Swift 中最常见的反应式框架是 RXSwift。我喜欢它的一点是它遵循了响应式编程模式。因此,如果您已经使用了 RxJava 、RxJS 或 RxKotlin ,您会感到更加熟悉。
Observable
RXSwift 通过 Observable 统一了同步和异步操作。你应该像下面这么做。1
2
3
4
5
6
7
8
9
10
11
12
13class 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
6override 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
15class 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 | class ProfileViewController: BaseViewController<ProfileView> { |
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 | enum Result<T> { |
推送信号
给出这样一个信号链: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 | public final class PushSignal<T> { |
下面是如何使用 PushSignal 将链从字符串转换为其长度,您应该看到 4,即打印的字符串 “test” 的长度。
1 | let signal = PushSignal<String>() |
获取信号
给出这样一个信号链: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 | public struct PullSignal<T> { |
链是不活动的,直到您调用链中的最后一个信号开始,这将触发操作流到第一个信号。运行这个代码,您应该看到 4 ,控制台上打印的字符串 “test” 的长度。1
2
3
4
5
6
7
8
9
10
11
12
13
14let 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 是一个强大的工具。以下是一些更有趣的读物:
- MVVM is Exceptionally OK
- Good iOS Application Architecture: MVVM vs. MVC vs. VIPER
- A Better MVC
- Taming Great Complexity: MVVM, Coordinators and RxSwift
- Rx — for beginners (part 9): Hot Vs. Cold observable
- Hot and Cold Observables
- When to use IEnumerable vs IObservable?
- Functional Reactive Programming without Black Magic
- Swift Sync and Async Error Handling