MVVM和反应式的味道–无瑕的应用故事–中

最初于 2018年8月16日 发布在 faultlessapp.io 博客上。

MVVM和反应式范例

我喜欢Swift,就像其他许多面向对象的编程语言一样。 Swift使您可以表示具有某些特征并可以执行某些操作的现实世界对象。

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

这可能不是一个好例子。 但是至少您了解OOP中进行通信和委派的重要性。 当我开始iOS编程时,我对“体系结构”一词很感兴趣。 但是经过一段时间后,一切都归结为确定和划分职责。 本文介绍了有关MVC和对MVVM的简单提取类重构的知识,以及如何使用Rx进行进一步开发。 您可以自由地创建自己的体系结构,但是无论您做什么,保持一致性都是避免混淆或使团队成员感到惊讶的关键。

模型视图控制器

看一下您最了解的MVC架构,它是Model View Controller的缩写。 创建新的iOS项目时,您总能得到一个。 View是您使用UIViewUIButtonUILabel 。 模型只是数据的幻想。 它可以是您的实体,网络中的数据,数据库中的对象或高速缓存中的对象。 控制器是在模型和视图之间中介的事物。

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的一部分)。它具有ViewModelLiveData ,这也是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上学习ViewModelRxSwift ,并且在Android上使用RxJavaRxBinding时感到宾至如归。 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,那么在使用一些框架一段时间后最好对它们有所了解。 有一些概念,例如SignalSignalProducerObservablePromiseFutureTaskJobLauncherAsync ,有些人对此有很大的区别。 在这里,我简单地称其为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发送事件时,它会通过回调传播该事件。 PushSignalPublishSubject中的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 ,诸如retryrebouncethrottlequeueflattenfilterdelaycombine和添加对UIKit支持。 了解如何在我的信号仓库中实施。

从这往哪儿走

建筑是一个很固执的话题。 希望本文能为您提供一些想法,以增加您的决策点。 MVC在iOS中占主导地位,MVVM是一个好朋友,Rx是一个强大的工具。 这是一些更有趣的读物:

  • MVVM非常好
  • 良好的iOS应用程序体系结构:MVVM与MVC与VIPER
  • 更好的MVC
  • 驯服极大的复杂性:MVVM,协调器和RxSwift
  • Rx-适用于初学者(第9部分):热门。 冷观测
  • 冷热观测
  • 何时使用IEnumerable和IObservable?
  • 无需Black Magic的功能性反应式编程
  • Swift同步和异步错误处理