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


以前に【iOS】RxSwiftでFluxを実装する際のちょっと痒いところの改善案でFluxのデータフローを単一方向に保つ改善案を書いてみました。
そちらでは、ActionとStore1つ実装すると、Dispatcherを1つ実装する想定でした。
今回は、Dispatcherが1つだった場合にデータフローを単一方向に保つ方法を解説していきたいと思います。

Dispatcherの実装

今回の場合、Dispatcherは1つになるので、実装しているStoreの個数分のChildDispatcherのPropertyを定義します。
ChildDispatcherは何かというと、observerとobservableを保持したGenerics classです。
Generics classとすることで、Dispatcherのextensionで定義しているDispatcher.SearchDispatcher.Messageを、observerとobservableを初期化する際のPublishSubject<Element>で利用できるようにしています。
Dispatcherで保持しているChildDispatcherのPropertyは、それぞれ1つずつのみインスタンス化されるようにしたいため、initializerがfileprivateになっています。

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

    let search = ChildDispatcher<Search>()
    let message = ChildDispatcher<Message>()

    private init() {}
}

final class ChildDispatcher<Element> {
    let observer: AnyObserver<Element>
    let observable: Observable<Element>

    fileprivate init() {
        let element = PublishSubject<Element>()
        self.observer = element.asObserver()
        self.observable = element
    }
}

Dispatcher.SearchDispatcher.Messageの列挙子は、それぞれを1つずつのDispatcherとした場合に定義することになるPropertyです。

Dispatcher.Search.swift
extension Dispatcher {
    enum Search {
        case items([Item])
        case error(Error)
        case lastItemsRequest(ItemsRequest)
    }
}
Dispatcher.Message.swift
extension Dispatcher {
    enum Message {
        case messages([Message])
        case error(Error)
        case lastOffset(Int)
    }
}

ActionやStoreでそれぞれに不要なものを隠蔽するDispatcherのラッパー

Storeからregisterのみを呼び出せるようにするため、RegisterableDispatcherを実装します。
RegisterableDispatcherのType parameterを<E, C: ChildDispatcher<E>>とし、Dispatcher.SearchやDispatcher.Messageを利用できるようにします。
initializerの引数としてChildDispatcherを受け取り、self.observableをChildDispatcherのobservableで初期化します。
func register(_ using: ((E) -> Void)?) -> Disposableを呼ぶと、そのobservableをsubscribeしています。
同様にして、Actionからdispatchのみを呼び出せるようにするため、DispatchableDispatcherを実装します。
initializerの引数としてChildDispatcherを受け取り、self.observerをChildDispatcherのobserverで初期化します。
func dispatch(_ element: E)を呼ぶと、そのobserverでonNextしています。

Dispatcher.swift
final class RegisterableDispatcher<E, C: ChildDispatcher<E>> {
    private let observable: Observable<E>

    init(_ child: C) {
        self.observable = child.observable
    }

    func register(_ using: ((E) -> Void)?) -> Disposable {
        return observable.subscribe(onNext: using)
    }
}

final class DispatchableDispatcher<E, C: ChildDispatcher<E>> {
    private let observer: AnyObserver<E>

    init(_ child: C) {
        self.observer = child.observer
    }

    func dispatch(_ element: E) {
        observer.onNext(element)
    }
}

利用例

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

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

    private let searchDispatcher: DispatchableDispatcher<Dispatcher.Search>
    private let searchStore: SearchStore
    private let session: QiitaSession
    private let disposeBag = DisposeBag()

    init(
        searchDispatcher: DispatchableDispatcher<Dispatcher.Search> = .init(Dispatcher.shared.search),
        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)

        //DispatchableDispatcher<Dispatcher.Search>なので、dispatchしか見えない
        searchDispatcher.dispatch(.lastItemsRequest(request))

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

同様にSearchStoreでRegisterableDispatcher<Dispatcher.Search>のProeprtyを持ち、初期化時にDispatcher.shared.searchを引数として渡すことで、registerのみを行うことができるDispatcherとして利用できるようになります。

SearchStore
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: RegisterableDispatcher<Dispatcher.Search> = .init(Dispatcher.shared.search)) {

        //RegisterableDispatcher<Dispatcher.Search>なので、registerしか見えない   
        searchDispatcher.register { [unowned self] element in
                //〜〜
            }
            .addDisposableTo(disposeBag)
    }
}