Fluxはデータフローを単一方向に限定するのが特徴的な設計パターンです。
SwiftでRxSwiftを利用してFluxを実現する際にに、下記のような問題点があると思います。
- Actionからsubscribeしたり、StoreからonNextができてしまう
- Storeが保持している値をViewからも変えることができてしまう
これらの問題を解決するためにはどのような実装にしていくかを解説していきたいと思います。
Actionからsubscribeしたり、StoreからonNextができてしまう
RxSwiftでイベントをDispatchするために、下記のようにPublishSubjectとしてPropertyを定義するかと思います。
final class SearchDispatcher {
static let shared = SearchDispatcher()
let items = PublishSubject<[Item]>()
let error = PublishSubject<Error>()
let lastItemsRequest = PublishSubject<ItemsRequest>()
init() {}
}
PublishSubjectであるということは
-
ObservableTypeなので
func subscribe<O: ObserverType>(_ observer: O) -> Disposable where O.E == E
を呼べる -
ObserverTypeなので
func onNext(_ element: E)
を呼べる
ということになります。
つまり、Fluxのデーターフローは単方向であるにも関わらず
- ActionからDispatcherにsubscribe
- StoreからDispatcherにonNext
ができてしまうということになります。
AnyDispatcherを利用して解決する
ActionからはonNext、Storeからはsubscribeしかできないという状態にするために、Observer
なDispatcherとObservable
なDispatcherのラッパーを実装していきます。
まずはDispatchableというProtocolです。
Dispatchableで重要な部分は
-
AnyObserver<StateType>
型のobserverState -
Observable<StateType>
型のobservableState
の実装を強制しているところです。
StateTypeはDispatchableを採用したオブジェクトで、enumになる想定をしています。
Protocolでinitializerをデフォルト実装することができないので、propertiesの中で必要なpropertyを生成しています。
protocol Dispatchable {
associatedtype StateType
static var shared: Self { get }
var observerState: AnyObserver<StateType> { get }
var observableState: Observable<StateType> { get }
init()
}
extension Dispatchable {
static func properties() -> (observer: AnyObserver<StateType>, observable: Observable<StateType>) {
let state = PublishSubject<StateType>()
return (state.asObserver(), state)
}
}
実際にDispatchableを採用したSearchDispatcherクラスは下記のようになります。
SearchDispatcher①でPublishSubjectとして定義していたPropertyを、StateとしてAssociated Value Enumにしています。
このenumをAnyObserver<State>
、Observable<State>
とすることで、DispatchableのStateTypeが確定します。
initializerでは、Dispatchableのpropertiesを使用して、observerStateとobservableStateを初期化します。
final class SearchDispatcher: Dispatchable {
enum State {
case items([Item])
case error(Error)
case lastItemsRequest(ItemsRequest)
}
static let shared = SearchDispatcher()
let observerState: AnyObserver<State>
let observableState: Observable<State>
required init() {
(self.observerState, self.observableState) = SearchDispatcher.properties()
}
}
ここまでの実装では、observerとobservableにPropertyを分けただけで、PublishSubjectでPropertyを定義しているのと対して差はありません。
そこで、SearchDispatcher②を
- AnyObserverDispatcher...
Observer
なDispatcher - AnyObservableDispatcher...
Observable
なDispatcher
として扱えるようにします。
AnyObserverDispatcherではAnyObserver型のstateを定義し、DispatcherTypeのobserverStateで初期化します。
AnyObservableDispatcherではObservable型のstateを定義し、DispatcherTypeのobservableStateで初期化します。
DispatcherType.StateTypeはDispatchableのStateTypeになります。
よって、SearchDispatcher②の場合、StateTypeはStateになります。
final class AnyObserverDispatcher<DispatcherType: Dispatchable>: ObserverType {
public typealias E = DispatcherType.StateType
private let state: AnyObserver<E>
init(_ dispatcher: DispatcherType = .shared) {
self.state = dispatcher.observerState
}
public func on(_ event: Event<E>) {
state.on(event)
}
func dispatch(_ value: E) {
on(.next(value))
}
}
final class AnyObservableDispatcher<DispatcherType: Dispatchable>: ObservableType {
public typealias E = DispatcherType.StateType
private let state: Observable<E>
init(_ dispatcher: DispatcherType = .shared) {
self.state = dispatcher.observableState
}
public func subscribe<O : ObserverType>(_ observer: O) -> Disposable where O.E == E {
return state.subscribe(observer)
}
}
それでは、AnyObserverDispatcherをActionで利用して、onNextだけが呼べるDispatcherとしていきます。
AnyObserverDispatcherのinitializerはDispatchableになっているので、SearchDispatcher②のインスタンスを渡します。
よって、searchDispatcherはsubscribeはできないDispatcherになります。
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>なので、state.onNextなどのObserverTypeしか見えない
searchDispatcher.dispatch(.lastItemsRequest(request))
session.send(request)
.subscribe(onNext: {
//〜〜
})
.addDisposableTo(disposeBag)
}
}
次に、AnyObservableDispatcherをStoreで利用して、subscribeだけが呼べるDispatcherとしていきます。
AnyObservableDispatcherのinitializerはDispatchableになっているので、SearchDispatcher②のインスタンスを渡します。
よって、searchDispatcherはonNextはできない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: AnyObservableDispatcher<SearchDispatcher> = .init(.shared)) {
//AnyObservableDispatcher<SearchDispatcher>なので、state.subscribeなでのObservableTypeしか見えない
searchDispatcher.subscribe(onNext: { [unowned self] state in
//〜〜
})
.addDisposableTo(disposeBag)
}
}
このように実装することによって、ActionからはonNext、Storeからはsubscribeしかできないという状態にでき、データーフローを単一方向に保つことができます。
この実装は、RxAnyDispatcherとしてGithubで公開しています。
Storeが保持している値をViewから変えることができてしまう
先程のサンプルのようにStoreで値を保持するために、VariableとしてPropertyを定義すると思います。
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>なので、state.subscribeなでのObservableTypeしか見えない
searchDispatcher.subscribe(onNext: { [unowned self] state in
//〜〜
})
.addDisposableTo(disposeBag)
}
}
Variableであるということは
SearchStore.shared.items.value = []
のように値を変更することができてしまいます。
つまり、Fluxのデーターフローは単方向であるにも関わらず、ViewからStoreの値を変更できてしまいます。
RxPropertyを利用して解決する
inamiyさんが公開しているRxPropertyを利用することで、get-onlyなSearchStore.shared.items.value
にすることができます。
例として、_itemsをprivateなVariableとして定義しつつ、itemsをProperty型として定義します。
_itemsは空の配列をVariableに渡して初期化をし、itemsは_itemsをProperty型に渡して初期化をします。
searchDispatcherをsubscribeして受け取った値を_items.valueで更新をし、Viewではitemsからsubscribeすることが可能になります。
final class SearchStore {
static let shared = SearchStore()
let items: Property<[Item]>
private let _items = Variable<[Item]>([])
let error: Property<Error?>
private let _error = Variable<Error?>(nil)
let lastItemsRequest: Property<ItemsRequest?>
private let _lastItemsRequest = Variable<ItemsRequest?>(nil)
private let disposeBag = DisposeBag()
init(searchDispatcher: AnyObservableDispatcher<SearchDispatcher> = .init(.shared)) {
self.items = Property(_item)
self.error = Property(_error)
self.lastItemsRequest = Property(_lastItemsRequest)
searchDispatcher.subscribe(onNext: { [unowned self] state in
switch state {
case .items(let value):
if value.isEmpty {
self._item.value.removeAll()
} else {
self._item.value.append(contentsOf: value)
}
case .error(let value):
self._error.value = value
case .lastItemsRequest(let value):
self._lastItemsRequest.value = value
}
})
.addDisposableTo(disposeBag)
}
}
このように実装することによって、Viewからはsubscribeしかできないという状態にでき、データーフローを単一方向に保つことができます。
さらに詳しい内容については4/26(水)に開催される『モバイルアプリ開発エキスパート養成読本』出版記念 Tech TalksでLTする予定です。
2017/04/06追記
こちらの記事は複数のDispatcherを想定したものになります。
Dispathcher自体を1つにする場合の改善案は、【iOS】FluxのDispatcherを1つでデータフローを単一方向に保つ案に記載しています。