A taste of MVVM and Reactive paradigm
一次Reactive和MVVM的编程范式尝试
原文地址
我喜欢Swift, 就像其他许多面向对象的编程语言一样,Swift会允许你去用各种特征或者行为来模拟真实世界。
我认为一个App就是一个世界,而其中的每一个对象就是一个人。他们工作,交流。如果某个人不想孤单工作,那么他就需要去寻求帮助。就拿一个项目来说,打个比方,如果管理者要把所有的工作包揽,那么他是活得不耐烦了。所以我们需要去组织和分配工作。这需要许多人在项目之中合作分工: 比如设计师,测试,经验丰富的工程师和普通开发者。当其他的任务被完成的时候,管理者必须被告知任务被完成了。
这可能不是一个好的例子。不过至少,这样你能明白了交流和代理在OOP中的重要性。从我开始iOS编程的时候,我对 “架构” 这个词非常好奇。 但在我工作了一段时间之后,这一切都变成了识别和分配能力。这篇文章希望能告知一些关于MVC的知识,并且通过一些额外的Class将其改造成MVVM, 并且还会同时介绍如何走向Rx。你想怎么建造你的架构是你的自由,但是无论如何,为了不要误导或者吓到他们,你最好能保持编程的一致性。
Model View Controller
看一下你所知道的最优秀的架构, MVC。View是你展示Data的地方,这里会包含UIView UIButton UILabel之类的控件。Model 就是你的Data, 他可以是来自网络或者数据库或者缓存中的的data. ViewController就是其中的协调者。
[站外图片上传中...(image-866995-1549950646350)]
UIViewController是一切的中心
ViewController最大的问题就是,他容易变得臃肿。Apple把他置于一个非常中心的位置。这意味着他会又许多属性和责任。 当然有一些事情你是只能通过UIViewController来处理的,比如和Storyboard 交互,管理View, 管理旋转事件。UIViewController就是设计为有许多功能让你去重写的。
稍微过一下UIViewController的文档,你就会发现,以下事情是你必须要通过UIViewController完成的
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
随着App的壮大,我们需要去增加许多的逻辑代码。 比如网络,数据源,处理多方的代理,展示子Controlelr. 我们当然可以把这些事件直接让UIViewController直接去做,不过这样会让他变得非常的臃肿。并且会非常锻炼你滚动屏幕的能力。把所有的事情都交给UIViewController来做会让你失去宏观上分配指责的能力。如此一来,你会更倾向复制代码,并且bug会不易被修复,因为他们都堆积在一个地方.
架构界的流行语
当你的ViewController变得庞大了,你会怎么做?有人会把网络的工作挪到其他组件。顺便说一下,如果你希望有其他的对象来处理用户的手动输入,你可以用Presenter.如果Presenter也做了太多事情,那么有人会把业务逻辑搬到Interactor.同时,以下还有许多流行语供你选择
let buzzWords = [
"Model", "View", "Controller", "Entity", "Router", "Clean", "Reactive",
"Presenter", "Interactor", "Megatron", "Coordinator", "Flow", "Manager"
]
let architecture = buzzWords.shuffled().takeRandom()
let acronym = architecture.makeAcronym()
[站外图片上传中...(image-1b9e95-1549950646350)]
实用主义程序员
人们对于什么是好的架构总是有着不同的理解。对于我而言,这个问题是
明确的指责分离,好的交互模式,易用。
架构中的每一个组件都应该可以被确认,并且有特定的功能。交互必须要清晰以便我们了解对象之间是怎么协调的。这些东西利用依赖注入在一起之后会让测试变得更加容易。
理论上听起来不错的事情,在实际中并不一定好用。分离指责听起来很酷,协议扩展好想也不错,多层抽象也很牛皮,不过这之中有太多的问题了。
如果你已经阅读过足够多的设计模式,那么你一定知道他们无非都是在讲这些事:
- 差异点的封装:把你的应用里的差异点封装成一个同样的问题。
- 用接口来编程,而不是靠实现来编程(这句话不错)
- 合成比继承好
如果说有一件事我们必须精通,那么就是合成了。这是指责分离并且合并起来的关键点。和你的同事的咨询,并讨论出一个适合的模式。总是用一种你会是今后维护者的想法来编程,这样的代码写出来可能会稍有不同。
不要跟系统做对
有一些架构会做一些新的编程范例。有一些人会写脚本去生成代码,造成代码很笨重。对于一个问题的处理有许多方式,不过对于我来说,这个感觉就像是在跟系统做对。我们不能仅仅因为一个架构时髦就去把自己困在里面,务实是最重要的。
在iOS的世界里,我们应该拥抱MVC. UIViewController不是屏幕内容。他可以被其他包含,也可以由其他组合,以此来实现分离指责的功能。我们可以用Coordinator或者FlowController来管理依赖或者控制流(Flow). 状态转换的容器,嵌入式逻辑控制器,屏幕内容的一部分。这种拥抱ViewController的方法可以很好地与iOS中的 MVC配合,也是我最喜欢的方式。
Model View ViewModel
[站外图片上传中...(image-18a8ab-1549950646350)]
至于另外一种把任务卸载给其他对象的方法,叫ViewModel.
这个名字并不重要,你可以叫他Reactor, Maestro, Dinosaur. 重要的是你的团队有一个一致的名字,ViewModel会接管部分来自ViewController的任务,并且在完成之后回馈给ViewController.Cocoa Touch里有许多交流模式,比如Delegate(代理), closure(闭包)
ViewModel是自立门户的,对于UIKit没有依赖,并且只有input和output. 我们可以把许多事情交给ViewModel去做,比如计算,格式化,网络,业务逻辑。当然,如果你不希望你的ViewModel变得太臃肿,你还需要去创建一些专用的对象。ViewModel只是你去创建一个Slim ViewController的第一步。
同步(Synchronously)
下面是一个非常简单的根据user Model 格式化数据的ViewModel, 他做到了同步 .
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
}
}
异步(Asynchronously)
我们总是会和异步API打交道。比如我们想要去展示一下我们的Facebook好友。为此,我们肯定是要去调用一下Facebook的API.这个API是不会立刻返回的,下面的这个ViewModel给我们展示里用闭包回调的方法。
viewModel.getFacebookFriends { friends in
self.friendCountLabel.text = "\(friends.count)"
}
在Viewmodel里,这个任务会被下发给专用的FacebookAPI对象
class ViewModel {
func getFacebookFriends(completion: [User] -> Void) {
let client = APIClient()
client.getFacebookFriends(for: user) { friends in
DispatchQueue.main.async {
completion(friends)
}
}
}
}
绑定(Binding)
为了封装闭包,我们可以创建一个可以通知多个监听者的叫Binding的类。这个功能主要是靠didSet的实现的。
class Binding {
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中,我们这样使用.
class ViewModel {
let friends = Binding<[User]>(value: [])
init() {
getFacebookFriends {
friends.value = $0
}
}
func getFacebookFriends(completion: ([User]) -> Void) {
// Do the work
}
}
当friends被获取,或者变更的时候。ViewController会同时更新,这就是reaction.
override func viewDidLoad() {
super.viewDidLoad()
viewModel.friends.bind { friends in
self.friendsCountLabel.text = "\(friends.count)"
}
}
我们通常会在MVVM的简介里面看到reactive框架,这是有道理的。这个框架提供非常多的链式操作符,让编程变得更加简单和陈述性。
RxSwift
在Swift中最常见的reactive框架就是RxSwift了。和RxJava,RxJs,RxKotlin都很相似。
[站外图片上传中...(image-9264e2-1549950646350)]
RxSwift通过Obserable统一了同步和异步操作。 下面是你如何创建的示范
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的强大在于他众多的运算符。它可以帮助我们连接Observable, 在这里你可以创建两个网络请求,等到他们一起结束,之后把结果合并。这个操作非常流线型,并且节约时间。在这里,你只要订阅Observable, 它就会在请求结束的时候被触发。
override func viewDidLoad() {
super.viewDidLoad()
viewModel.friends.subscribe(onNext: { friends in
self.friendsCountLabel.text = "\(friends.count)"
})
}
Input 和 output
Rxswift提供了非常简洁的接口,让我们可以通过Obserable分离input和output.
下面的fetch是一个input, friends是一个可以获取的output.
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
}
}
class ProfileViewController: BaseViewController {
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.
Monad(单子)
Signal和它的Result都是monads,可以被map和chain.
Signal使用延迟执行的闭包。他可以被push或者pull来更新值和执行顺序.
延迟执行意味着我们传输一个会在将来的某个事件被执行的function .
同步VS异步
Monad可以处于同步或者异步模式。
总的来说:
- 同步: 立刻得到返回值
- 异步: 通过回调得到返回值
下面是一个同步异步的方法
// 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)
}
那么同步和异步是如何都被当作是Result类型呢? 注意在使用异步的时候,我们在闭包里得到一个计算后的值,而不是立刻返回。
enum Result {
case value(value: T)
case failure(error: Error)
// Sync
public func map(f: (T) -> U) -> Result {
switch self {
case let .value(value):
return .value(value: f(value))
case let .failure(error):
return .failure(error: error)
}
}
// Async
public func map(f: @escaping ((T), (U) -> Void) -> Void) -> (((Result) -> Void) -> Void) {
return { g in // g: Result -> 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))
}
}
}
}
Push Signal
给出这样的链式signals
A -(map)-> B -(flatMap)-> C -(flatMap)-> D -(subscribe)
Push signal意味着当signal A被发送的时候,他通过回调传播事件。PushSignal相当于在RxSwift里的PublishSubject
- 通过发送事件被触发。
- 通过持有A来保持其他。
- 订阅最后一个事件D
- 发送第一个事件A
- 当A的回调结束时,同时会执行B的回调,并一直传递下去.
下面是一个Swift4版本的PushSignal实现
public final class PushSignal {
var event: Result?
var callbacks: [(Result) -> 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) {
lockQueue.sync {
self.event = event
}
notify()
}
public func subscribe(f: @escaping (Result) -> Void) -> Signal {
// Callback
if let event = event {
f(event)
}
callbacks.append(f)
return self
}
public func map(f: @escaping (T) -> U) -> Signal {
let signal = Signal()
_ = subscribe { event in
signal.update(event: event.map(f: f))
}
return signal
}
}
下面是一个PushSignal使用作计算字符串长度
let signal = PushSignal()
_ = 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"))
Pull Signal
给出一个如下的链式signal
A -(map)-> B -(flatMap)-> C -(flatMap)-> D -(subscribe)
Pull Signal, 有时候被叫做Future. 他会在你订阅D的时候,调用之前的Signal
- 由订阅D触发。
- 持有D, 因为D持有其他对象。
- 必须去订阅最后一个D
- D的操作引发C的动作。。 最终引发A的动作。
下面是Swift4版本的Pullsignal. PullSignal类似于RxSwift里的Observable. 或者ReactiveSwift里的SignalProducer.
public struct PullSignal {
let operation: ((Result) -> Void) -> Void
public init(operation: @escaping ((Result) -> Void) -> Void) {
self.operation = operation
}
public func start(completion: (Result) -> Void) {
operation() { event in
completion(event)
}
}
public func map(f: @escaping (T) -> U) -> PullSignal {
return PullSignal { completion in
self.start { event in
completion(event.map(f: f))
}
}
}
}
这条链式会当你在链条的末端start时运行。下面还是一段测试代码
let signal = PullSignal { 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")
}
}
我希望这些代码片段可以帮助你理解signal工作,理解冷信号和热信号。如果想完全理解signal. 你需要去实现更多的操作符,比如retry, rebounce, throttle, queue, faltten, filter, delay, combine. 并且支持对UIKit的操作,比如RxCocoa. 也可以在我的仓库里查看他们的实现。
下一步怎么走?
架构是一个非常个性化的话题。希望本文可以给你一些帮助。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