以前に【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.Search
やDispatcher.Message
を、observerとobservableを初期化する際のPublishSubject<Element>
で利用できるようにしています。
Dispatcherで保持しているChildDispatcherのPropertyは、それぞれ1つずつのみインスタンス化されるようにしたいため、initializerがfileprivateになっています。
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.Search
やDispatcher.Message
の列挙子は、_それぞれを1つずつのDispatcherとした場合に定義することになるProperty_です。
extension Dispatcher {
enum Search {
case items([Item])
case error(Error)
case lastItemsRequest(ItemsRequest)
}
}
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しています。
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として利用できるようになります。
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として利用できるようになります。
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)
}
}