LoginSignup
34
25

More than 5 years have passed since last update.

【iOS】RxSwiftでFluxを実装する際のちょっと痒いところの改善案①

Last updated at Posted at 2017-04-04

Fluxはデータフローを単一方向に限定するのが特徴的な設計パターンです。
SwiftでRxSwiftを利用してFluxを実現する際にに、下記のような問題点があると思います。

  • Actionからsubscribeしたり、StoreからonNextができてしまう
  • Storeが保持している値をViewからも変えることができてしまう

これらの問題を解決するためにはどのような実装にしていくかを解説していきたいと思います。

flux.png

Actionからsubscribeしたり、StoreからonNextができてしまう

RxSwiftでイベントをDispatchするために、下記のようにPublishSubjectとしてPropertyを定義するかと思います。

SearchDispatcher①
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を生成しています。

Dispatchable
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を初期化します。

SearchDispatcher②
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になります。

AnyDispatcher
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になります。

SearchAction
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になります。

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: 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を定義すると思います。

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: 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することが可能になります。

SearchStore②
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つでデータフローを単一方向に保つ案に記載しています。

34
25
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
34
25