A taste of MVVM and Reactive paradigm(翻译)

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

你可能感兴趣的:(A taste of MVVM and Reactive paradigm(翻译))