【译】A taste of MVVM and Reactive paradigm

阅读 193
收藏 5
2019-06-28
原文链接:mp.weixin.qq.com

来自周报编辑水水的一篇文章,点击阅读原文效果更佳~

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,则无法执行以下操作。

func viewDidLoad()var preferredStatusBarStyle: UIStatusBarStyle { get }UITableViewDataSourcevar presentationController: UIPresentationController? { get }func childViewControllerForScreenEdgesDeferringSystemGestures() -> UIViewController?func didMove(toParentViewController parent: UIViewController?)var systemMinimumLayoutMargins: NSDirectionalEdgeInsetsvar edgesForExtendedLayout: UIRectEdgevar previewActionItems: [UIPreviewActionItem]var navigationItem: UINavigationItemvar shouldAutorotate: Bool

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

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 配合使用,是我的首选方法。

MVVM

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

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

同步

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

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.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

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

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 的优点,使其可观测性变得清晰。

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 示例:

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 统一了同步和异步操作。你应该像下面这么做。

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 监听,当请求完成时会触发它:

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 是可行的输出。

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<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 可以是同步模式或异步模式。同步更容易理解,但异步在实践中已经很熟悉和使用了。

  • 同步:

    通过返回立即得到返回值

  • 异步:

    通过回调块得到返回值

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

// Syncfunc sum(a: Int, b: Int) -> Int {    return a + b}// Asyncfunc 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)}

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

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 实现。

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” 的长度。

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。

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” 的长度。

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 是一个强大的工具。以下是一些更有趣的读物:

  • 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

评论