【はじめに】
RxSwiftは素晴らしく、社内のSwift開発者の間でもメインストリームになりつつあります。
実際自分もRxSwiftを業務に採用したことで色々な知見が得られ、また開発効率が大きく上がったと感じています。
まずRxSwiftは素晴らしく、今後とも使い続けたい、ということは最初に述べておきたいと思います。
しかし、自作のアプリケーション・フレームワークを模索・構築している中で、自分が必須と考えている各レイヤー間の非同期通信手段というものに着目して再考した時に、RxSwiftは少しばかりオーバースペックではないかと考えるようになりました。何よりも自作のフレームワークの中にこのRxSwiftを中枢機能として組み込むことに抵抗を感じた、ということもあります。
そこで軽量な非同期通信手段をRxSwiftっぽく作ることはできないかと考え、Swiftの勉強がてら自作してみることにしました。
【主な機能】
主な機能は以下の通りです。
- RxSwiftと同様にObservableがあり、それをsubscribeすることで非同期に値を持つイベントを取得することができる。もちろんクロージャで記述します。
- subscribe時にイベントの取得を一度だけに限定できる。(デフォルトは限定なし)
- subscribe時に過去の最新のイベントを取得するかどうかを設定できる。(デフォルトは次回のイベントから取得)
- RxSwiftと同じ記法で書けるDisposeBagクラスを持つので、RxSwiftと同様にObserverであるインスタンス(subscribeしたやつ)が破棄されると同時にsubscribeを廃棄することができる。
【実装】
subscribe
まずはsubscribeメソッドから説明します。
subscribeでは、イベントの取得を一度だけに限定するかどうかを決めるonceがあり、過去の最新のイベントを取得するかどうかを決めるreturnLatesteを引数として与えることができます。これによってsubscribeする側が、同じObservableに対してsubscribeの方法を決定することができるようになりました。自分では地味に便利な機能だと思っています。
このことの意味はObservableを持つ側(一般的に下層モジュール)は、(上位層の)使われ方を意識することなく、Observableを画一的に宣言することができ、よりレイヤー間の接合性を弱めることができるということだと考えています。
/// subscribe
///
/// - Parameters:
/// - observer: BwObserver that observe this BwObservable. This is just identifier of observer.
/// - once: subscribe once
/// - returnLateste: immediately return contens, if it already exists.
/// - action: Closure that is invoked when contents are published
/// - Returns: BwDeliverable?
public func subscribe(_ observer: BwObserver, once: Bool = false, returnLateste: Bool = false, action: @escaping ((_ contents: ContentsType ) -> Void) ) -> BwDeliverable?
{
if returnLateste, let _latestContents = latestContents
{
DispatchQueue.main.async {
action(_latestContents)
}
if once { return nil }
}
let deliverable = BwDeliverable(observer, observable: self, once: once, action: action)
deliverables.append(deliverable)
return deliverable
}
publish
publishはイベントを発行側が使用します。ContentsType型のイベントをsubscribeしているobserverに配信することができます。配信後に一度だけしか配信を希望しないObserverは削除されるこtになります。
/// Execute action closures
///
/// - Parameter contents: Contents to be published to subscribers(observers)
public func publish(_ contents: ContentsType)
{
latestContents = contents
for deliverable in deliverables
{
deliverable.action(contents)
}
deliverables = deliverables.filter({!$0.once})
}
では配信を管理するためのクラスはどうなっているのでしょうか。下記にDeliverableクラスを記載します。
Deliverableは、subscribe1回につき1つ生成されて、Deliverable配列の記憶されます。
上記のpublishメソッド内のdeliverablesがそれです。
publishメソッド内で実行されるactionクロージャなどが宣言されています。
Deliverable
Deliverableクラスはsubscribeを廃棄することが可能なDisposableプロトコルを実装しています。
また配信のためのContentsType型は最後に記述するようにGenericsとなっています。
public func disposed( by disposeBag: BwDisposeBag)
によってDisposeBagに自らを登録します。
public func dispose()
はDisposableプロトコルが要求する実装となります。
/// Class for returning observing results
public class BwDeliverable: Disposable
{
var action: ((_ result: ContentsType ) -> Void)
var observer: BwObserver
var once: Bool = false
var observable: BwObservable<ContentsType>?
required public init(_ observer: BwObserver, observable: BwObservable<ContentsType>, once: Bool, action: @escaping ((ContentsType) -> Void)) {
self.observer = observer
self.once = once
self.action = action
self.observable = observable
}
public func disposed( by disposeBag: BwDisposeBag)
{
disposeBag.set(self)
}
public func dispose() {
self.observable?.dispose(by: observer)
}
deinit
{
print("deinit")
}
}
説明が逆転していますが、Observableクラスの残りの部分を記述すると以下のようになります。
Observable
public class BwObservable<ContentsType>
{
// 上で説明したクラスがこの辺りに入る
/// Discard all BwDeliverable that own the observer specified in the instance
///
/// - Parameter observer: observer
public func dispose(by observer: BwObserver)
{
deliverables = deliverables.filter({!($0.observer === observer)})
}
}
ContentsTypeという型を与えられてイベントで配信するべき型が決定します。
またsubscribeしているObserverが消える時に呼びだされてsubscribeを抹消するdisposeメソッドがあります。
その他のBwObserverの定義とBwDisposeBagクラスを追記した全体は下記のようになります。
自分では、コンパクトかつシンプルな分かり易い実装になっていると思います。
import Foundation
/// Because this is just an identifier, AnyObject can be
public typealias BwObserver = AnyObject
/// Class for canceling subscribe when subscribed instance is destroyed
public class BwDisposeBag
{
private var deliverables = [Disposable]()
public init() {
}
fileprivate func set(_ deliverable: Disposable)
{
deliverables.append(deliverable)
}
deinit {
for deliverable in deliverables
{
deliverable.dispose()
}
}
}
/// A protocol with a method for canceling subscribe all at once
fileprivate protocol Disposable
{
func dispose()
}
/// A class that can be observed to return a specific results by closure
public class BwObservable<ContentsType>
{
/// Class for returning observing results
public class BwDeliverable: Disposable
{
var action: ((_ result: ContentsType ) -> Void)
var observer: BwObserver
var once: Bool = false
var observable: BwObservable<ContentsType>?
required public init(_ observer: BwObserver, observable: BwObservable<ContentsType>, once: Bool, action: @escaping ((ContentsType) -> Void)) {
self.observer = observer
self.once = once
self.action = action
self.observable = observable
}
public func disposed( by disposeBag: BwDisposeBag)
{
disposeBag.set(self)
}
public func dispose() {
self.observable?.dispose(by: observer)
}
deinit
{
print("deinit")
}
}
private var deliverables: [BwDeliverable] = []
private var latestContents: ContentsType?
public init() {}
/// subscribe
///
/// - Parameters:
/// - observer: BwObserver that observe this BwObservable. This is just identifier of observer.
/// - once: subscribe once
/// - returnLateste: immediately return contens, if it already exists.
/// - action: Closure that is invoked when contents are published
/// - Returns: BwDeliverable?
public func subscribe(_ observer: BwObserver, once: Bool = false, returnLateste: Bool = false, action: @escaping ((_ contents: ContentsType ) -> Void) ) -> BwDeliverable?
{
if returnLateste, let _latestContents = latestContents
{
DispatchQueue.main.async {
action(_latestContents)
}
if once { return nil }
}
let deliverable = BwDeliverable(observer, observable: self, once: once, action: action)
deliverables.append(deliverable)
return deliverable
}
/// Execute action closures
///
/// - Parameter contents: Contents to be published to subscribers(observers)
public func publish(_ contents: ContentsType)
{
latestContents = contents
for deliverable in deliverables
{
deliverable.action(contents)
}
deliverables = deliverables.filter({!$0.once})
}
/// Discard all BwDeliverable that own the observer specified in the instance
///
/// - Parameter observer: observer
public func dispose(by observer: BwObserver)
{
deliverables = deliverables.filter({!($0.observer === observer)})
}
}
実際に動くサンプルとしては、
https://github.com/BlueEventHorizon/BwObservable/blob/master/BwObservableExample/BwObservable/BwObservable.swift
にアップしています。
実際に使用された方にバグ報告、要望、各種フィードバックなどしていただければ幸いです。
【余談】
MVVMのような大局的な設計において、RxSwiftの全ての機能を使う必要はないと考えていますが、局所的には使いたいこともあります。
例えばUITextFieldの入力イベントを取り出すのにdelegateで書くのは多少面倒です。RxSwift(この場合はRxCocoa)ならばクロージャ一発で記述できます。
しかしながら設計上の必要性と、View内の1パーツの処理が簡単に書けるという利点は、全くレベルが異なるものではないでしょうか。前者は設計上の重要項目を達成するために非常に重要であり、後者はたかが(失礼!)便利なユーティリティです。
このようなRxSwiftの持つ多様で次元が大きく異なる性質は、1つの機能をよく吟味して利用していかないと、全てをRxSwiftで書けばいいというわけでもないと思っています。
そのような文脈においても、本稿で紹介したクラスは面白い実装になったと考えています。