MVVM和反应式的味道–无瑕的应用故事–中
最初于 2018年8月16日 发布在 faultlessapp.io 博客上。
MVVM和反应式范例
我喜欢Swift,就像其他许多面向对象的编程语言一样。 Swift使您可以表示具有某些特征并可以执行某些操作的现实世界对象。
我倾向于将应用程序视为每个对象都是一个人的世界。 他们工作和沟通。 如果一个人不能独自完成工作,则需要寻求帮助。 以一个项目为例,如果经理必须独自完成所有工作,他就会发疯。 因此,需要组织和委派任务,并且需要许多人在项目上进行协作:设计师,测试人员,Scrum管理员,开发人员。 任务完成后,需要通知经理。
这可能不是一个好例子。 但是至少您了解OOP中进行通信和委派的重要性。 当我开始iOS编程时,我对“体系结构”一词很感兴趣。 但是经过一段时间后,一切都归结为确定和划分职责。 本文介绍了有关MVC和对MVVM的简单提取类重构的知识,以及如何使用Rx进行进一步开发。 您可以自由地创建自己的体系结构,但是无论您做什么,保持一致性都是避免混淆或使团队成员感到惊讶的关键。
模型视图控制器
看一下您最了解的MVC架构,它是Model View Controller的缩写。 创建新的iOS项目时,您总能得到一个。 View是您使用UIView
, UIButton
和UILabel
。 模型只是数据的幻想。 它可以是您的实体,网络中的数据,数据库中的对象或高速缓存中的对象。 控制器是在模型和视图之间中介的事物。
UIViewController是宇宙的中心
ViewController
的问题在于它往往很大。 苹果将其作为宇宙的中心,在那里它具有许多特性和责任。 您只能使用UIViewController
做很多事情。 与情节提要板进行交互,管理视图,配置视图旋转,状态还原等操作。 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
随着您的应用程序的增长,我们需要为其他逻辑添加更多代码。 例如网络,数据源,处理多个委托,呈现子视图控制器。 当然,我们可以将所有内容放到视图控制器上,但这会导致使用大视图控制器并提高滚动技能。 在这里,您失去了责任感,因为所有内容都保留在mega view controller中。 您倾向于引入代码重复,并且由于遍布各处,因此难以修复错误。
Windows Phone中的Page
或Android中的Activity
也是如此。 它们用于功能的屏幕或部分屏幕。 有些动作只能通过它们来完成,例如Page.OnNavigatedTo,Activity.onCreate。
建筑学流行语
当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()
组装所有流行语之后,我们便得到了一个体系结构。 它们有很多,从简单的提取类重构,使用MVC或从Clean Code,Rx,EventBus或Redux汲取灵感。 选择取决于项目,有些团队更喜欢一种架构。
务实的程序员
人们对什么是好的架构有不同的看法。 对我来说,这是明确的关注点分离,良好的沟通方式和使用舒适的感觉。 体系结构中的每个组件都应该是可识别的,并具有特定的作用。 通讯必须清楚,以便我们知道哪个对象正在互相交谈。 加上良好的依赖注入,将使测试更加容易。
理论上听起来不错的事情在实践中可能效果不佳。 分离的域对象很酷,协议扩展很酷,多层抽象很酷。 但是它们太多可能是一个问题。
如果您对设计模式有足够的了解,就会知道它们全都归结为以下简单原则:
- 封装变化的内容:确定应用程序中变化的方面,并将其与保持不变的部分分开。
- 编程到接口,而不是实现
- 优先考虑组成而不是继承
如果我们要掌握一件事,那就是组成 。 关键是要确定责任并以合理和一致的方式将其组成。 请咨询您的队友最合适的。 在编写代码时始终以为自己将来也是维护者。 然后,您将以不同的方式编写它。
不要与系统作斗争
一些架构引入了全新的范例。 其中有些麻烦,因为人们编写脚本来生成样板代码。 有很多解决问题的方法很好。 但是对我而言,有时候感觉他们正在与系统对抗。 有些任务变得容易,而一些琐碎的任务却变得异常困难。 我们不应仅仅因为一种架构而将自己局限于一种架构。 要务实,不要教条。
在iOS中,我们应该拥抱MVC。 UIViewController
并非用于全屏内容。 它们可以包含并组成以拆分功能。 我们可以使用Coordinator和FlowController来管理依赖关系并处理流程。 用于状态转换的容器,嵌入式逻辑控制器,全屏内容的一部分。 这种包含ViewController的方法可以很好地与iOS中的MVC配合使用,这是我更可取的方法。
模型视图ViewModel
另一个很好的方法是将一些任务卸载到另一个对象,我们称之为ViewModel。 名称无关紧要,您可以将其命名为Reactor,Maestro,Dinosaur。 重要的是您的团队要获得一致的名称。 ViewModel从ViewController承担一些任务,并在完成时报告。 Cocoa Touch中有一些通信模式,例如委托,要使用的闭包。
ViewModel是独立的,没有对UIKit的引用,仅具有输入和输出。 我们可以在ViewModel中放入很多内容,例如计算,格式化,联网,业务逻辑。 另外,如果您不希望ViewModel变得庞大,则肯定需要创建一些专用对象。 ViewModel是获得苗条ViewController的第一步。
同步地
下面是一个非常简单的ViewModel,它根据User
模型格式化数据。 这是同步完成的。
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,此操作需要时间。 ViewModel可以通过闭包进行报告。
viewModel.getFacebookFriends { friends in
self.friendCountLabel.text = "\(friends.count)"
}
在内部,ViewModel可以将任务卸载到专用的Facebook API客户端对象。
class ViewModel {
func getFacebookFriends(completion: [User] -> Void) {
let client = APIClient()
client.getFacebookFriends(for: user) { friends in
DispatchQueue.main.async {
completion(friends)
}
}
}
}
Android中的Jetpack
Google在Google IO 2017上推出了Android Architecture Component(现已成为Jetpack的一部分)。它具有ViewModel
和LiveData
,这也是Android中应用的一种MVVM。 ViewModel
通过配置更改LiveData
,并根据LiveData
通知结果以供Activity
使用。
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
优势,observable属性变得清晰。
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(value: [])
init() {
getFacebookFriends {
friends.value = $0
}
}
func getFacebookFriends(completion: ([User]) -> Void) {
// Do the work
}
}
每当朋友被获取或更改时,ViewController都会相应更新。 这称为对变化的反应。
override func viewDidLoad() {
super.viewDidLoad()
viewModel.friends.bind { friends in
self.friendsCountLabel.text = "\(friends.count)"
}
}
您经常会看到带有响应式框架的MVVM简介,这是有原因的。 它们提供了许多链接运算符,并使响应式编程更容易且更具声明性。
RxSwift
Swift中最常见的反应框架可能是RxSwift。 我喜欢的是它遵循ReactiveX模式。 因此,如果您已经使用过RxJava,RxJs或RxKotlin,您会感到更加熟悉。
可观察的
RxSwift通过Observable
统一同步和异步操作。 这就是你的方式。
class ViewModel {
let friends: Observable
init() {
let client = APIClient()
friends = Observable.create({ subscriber in
client.getFacebookFriends(completion: { friends in
subscriber.onNext(friends)
subscriber.onCompleted()
})
return Disposables.create()
})
}
}
RxSwift的强大之处在于其众多的运算符,可帮助您链接Observable。 在这里,您可以调用2个网络请求,等待它们都完成,然后汇总朋友。 这非常简化,为您节省了很多时间。 在这里,您可以只订阅Observable,它将在请求完成时触发:
override func viewDidLoad() {
super.viewDidLoad()
viewModel.friends.subscribe(onNext: { friends in
self.friendsCountLabel.text = "\(friends.count)"
})
}
输入输出
关于ViewModel和Rx的一件好事是,我们可以使用Observable
分离输入和输出,这提供了清晰的界面。 在“从开源学习:输入和输出容器”中了解更多信息。
在下面很明显,我们fetch
是输入,而friends
是可行的输出。
class ViewModel {
class Input {
let fetch = PublishSubject()
}
class Output {
let friends: Driver
}
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)"
})
}
}
反应如何工作
如果您感觉像Rx,那么在使用一些框架一段时间后最好对它们有所了解。 有一些概念,例如Signal
, SignalProducer
, Observable
, Promise
, Future
, Task
, Job
, Launcher
, Async
,有些人对此有很大的区别。 在这里,我简单地称其为Signal
,它可以发出信号。
单子
Signal
及其结果仅仅是monad,它们是可以映射和链接的东西。
Signal
利用了延迟执行回调闭包。 可以推拉它。 这就是Signal
更新其值和调用回调顺序的方式。
执行回调闭包意味着我们将一个函数传递给另一个函数。 传入的函数将在适当时被调用。
同步与异步
Monad可以处于同步或异步模式。 同步更容易理解,但是异步在某种程度上已经为您所熟悉并已在实践中使用。
基本上,
- 同步:您可以通过
return
立即获得返回值 - 异步:通过回调块获取返回值
这是简单的同步和异步免费功能的示例:
// 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))
}
}
}
}
推送信号
给定这样的链接信号:
A -(map)-> B -(flatMap)-> C -(flatMap)-> D -(subscribe)
推送信号,意味着当向源信号A发送事件时,它会通过回调传播该事件。 PushSignal
与PublishSubject
中的PublishSubject相似。
- 通过将事件发送到源信号来触发。
- 我们必须保持A不变
- 我们订阅最后一个D
- 我们向第一个A发送事件
- 调用A的回调,然后依次使用A的
map
结果调用B的回调,然后B的回调使用B的flatMap
的结果调用C的回调,依此类推。
它类似于Promise A +,您可以在Then框架中看到Promise A +的Swift实现。 现在,这是一个简单的PushSignal.
的Swift 4实现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将链从字符串转换为其长度的方法,您应该会看到4,即打印的字符串“ test”的长度。
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"))
拉信号
给定这样的链接信号:
A -(map)-> B -(flatMap)-> C -(flatMap)-> D -(subscribe)
拉信号(有时称为Future
)意味着当我们订阅最终信号D时,它会使先前的信号起作用:
- 通过订阅最终信号D触发;
- 我们必须保持D不变,因为它会使其他周围存在。
- 我们订阅最后一个D;
- D的操作运行,并且导致C的操作运行,……然后A的操作运行。 在A中执行任务(例如获取网络,检索数据库,文件访问,大量计算等)以获取结果,然后调用A的完成。 然后,A的完成功能调用B的完成功能,其结果由B的映射映射,……一直到订户的完成功能块。
这是PullSignal
的Swift 4实现。 PullSignal
类似于PullSignal
中的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
之前,该链才处于活动状态,这将触发整个操作流向第一个信号。 运行此代码段,您将看到4,即控制台上打印的字符串“ test”的长度。
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
框架,您需要实施更多的操作。 如rebounce
,诸如retry
, rebounce
, throttle
, queue
, flatten
, filter
, delay
, combine
和添加对UIKit
支持。 了解如何在我的信号仓库中实施。
从这往哪儿走
建筑是一个很固执的话题。 希望本文能为您提供一些想法,以增加您的决策点。 MVC在iOS中占主导地位,MVVM是一个好朋友,Rx是一个强大的工具。 这是一些更有趣的读物:
- MVVM非常好
- 良好的iOS应用程序体系结构:MVVM与MVC与VIPER
- 更好的MVC
- 驯服极大的复杂性:MVVM,协调器和RxSwift
- Rx-适用于初学者(第9部分):热门。 冷观测
- 冷热观测
- 何时使用IEnumerable和IObservable?
- 无需Black Magic的功能性反应式编程
- Swift同步和异步错误处理