【iOS】FluxのDispatcherのデータフローを単一方向に保つ案③


以前に【iOS】RxSwiftでFluxを実装する際のちょっと痒いところの改善案①【iOS】FluxのDispatcherを1つでデータフローを単一方向に保つ案②でFluxのデータフローを単一方向に保つ改善案を書いてみました。
そちらでは、内部でPublishSubject<Element>のTypeParameterがenumであるものをDispatcherの中で1つ実装されている状態でした。
しかし、上記の場合はStoreでsubscribeをする際に、enumをswitch-case文で分岐をしてbindする必要がありました。
今回は、Dispatcherの中で複数のPublishSubjectを持ち、Storeでswitch-case文を使わずともbindできつつ、データフローを単一方向に保つ方法を解説していきたいと思います。

AnyObserverDispatcherとAnyObservableDispatcher

データフローを単一方向に保つために

  • onNextだけができるDispathcherのAnyObserverDispatcher
  • subscribeだけができるDispatcherのAnyObservableDispatcher

を実装します。

DispatcherTypeというprotocolを定義します。
DispatcherTypeではclass自身のシングルトンの実装を強制します。

AnyObserverDispatcherとAnyObservableDispatcher、それぞれのclassのTypeParameterのDispatcherがDispatcherTypeを採用している状態にします。
それぞれのclassでDispatcherを引数とし、そのインスタンスをpropertyとして保持します。

protocol DispatcherType {
    static var shared: Self { get }
}

final class AnyObserverDispatcher<Dispatcher: DispatcherType> {
    let dispatcher: Dispatcher
    init(_ dispatcher: Dispatcher = .shared) {
        self.dispatcher = dispatcher
    }
}

final class AnyObservableDispatcher<Dispatcher: DispatcherType> {
    let dispatcher: Dispatcher
    init(_ dispatcher: Dispatcher = .shared) {
        self.dispatcher = dispatcher
    }
}

Dispatcherの実装

まずDispatcherTypeを採用しstatic propertyを定義しつつ、必要な要素をfileprivateなPublishSubjectのPropertyとして定義していきます。
ここでfileprivateにする理由としては、SearchDispatcher自体ではonNextもsubscribeも行わないので、外からpropertyを見えなくするためです。

AnyObservableDispatcherのextensionをSearchDispatcherと同一ファイル内で定義します。
AnyObservableDispatcherのDispatcherがSearchDispatcherである場合は、それぞれのPropertyをObservableとして返すPropertyを定義します。

同じく、AnyObserverDispatcherのextensionをSearchDispatcherと同一ファイル内で定義します。
AnyObserverDispatcherのDispatcherがSearchDispatcherである場合は、それぞれのPropertyをObserverとして返すPropertyを定義します。

SearchDispatcher.swift
final class SearchDispatcher: DispatcherType {
    static let shared = SearchDispatcher()

    fileprivate let items = PublishSubject<[Item]>()
    fileprivate let error = PublishSubject<Error>()
    fileprivate let lastItemsRequest = PublishSubject<ItemsRequest>()

    private init() {}
}

extension AnyObservableDispatcher where Dispatcher: SearchDispatcher {
    var items: Observable<[Item]> {
        return dispatcher.items
    }
    var error: Observable<Error> {
        return dispatcher.error
    }
    var lastItemsRequest: Observable<ItemsRequest> {
        return dispatcher.lastItemsRequest
    }
}

extension AnyObserverDispatcher where Dispatcher: SearchDispatcher {
    var items: AnyObserver<[Item]> {
        return dispatcher.items.asObserver()
    }
    var error: AnyObserver<Error> {
        return dispatcher.error.asObserver()
    }
    var lastItemsRequest: AnyObserver<ItemsRequest> {
        return dispatcher.lastItemsRequest.asObserver()
    }
}

利用例

実際にSearchActionでAnyObserverDispatcherのProeprtyを持ち、初期化時にSearchDispatcher.sharedを引数として渡すことで、subscribeのみを行うことができるDispatcherとして利用できるようになります。

SearchAction.swift
final class SearchAction {
    static let shared = SearchAction()

    private let searchDispatcher: AnyObserverDispatcher<SearchDispatcher>
    private let searchStore: SearchStore
    private let session: QiitaSession
    private let disposeBag = DisposeBag()

    init(
        searchDispatcher: AnyObserverDispatcher<SearchDispatcher> = .init(.shared),
        searchStore: SearchStore = .shared,
        session: QiitaSession = .shared
    ) {
        self.searchDispatcher = searchDispatcher
        self.searchStore = searchStore
        self.session = session
    }

    func search(query: String? = nil) {
        //〜〜
        let request = ItemsRequest(page: nextPage, perPage: perPage, query: nextQuery)

        //AnyObserverDispatcher<SearchDispatcher>なので、onNextしか見えない
        searchDispatcher.lastItemsRequest.onNext(request)

        session.send(request)
            .subscribe(onNext: {
                //〜〜
            })
            .addDisposableTo(disposeBag)
    }
}

実際にSearchStoreでAnyObservableDispatcherをinitializerの引数とし、初期化時にSearchDispatcher.sharedを渡すことで、onNextのみを行うことができるDispatcherとして利用できるようになります。

SearchStore.swift
final class SearchStore {
    static let shared = SearchStore()

    let items = Variable<[Item]>([])
    let error = Variable<Error?>(nil)
    let lastItemsRequest = Variable<ItemsRequest?>(nil)
    private let disposeBag = DisposeBag()

    init(searchDispatcher: AnyObservableDispatcher<SearchDispatcher> = .init(.shared)) {

        //AnyObservableDispatcher<SearchDispatcher>なので、subscribeしか見えない   
        searchDispatcher.subscribe(onNext: { [unowned self] element in
                //〜〜
            })
            .addDisposableTo(disposeBag)
    }
}

最後に

Fluxのデータフローを単一方向に保つ改善案として今までに3つほど提案してみましたが、それぞれに良さがあるのでプロジェクトによってどのタイプを導入するかの参考になればと思います。