LoginSignup
13
7

More than 5 years have passed since last update.

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

Last updated at Posted at 2017-04-06

以前に【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)
    }
}
13
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
7