今日は、iOSエンジニアの@orakaroです。
iOSエンジニアの皆さん、iPhone Xの対応はいかがでしょうか? メルカリアッテはようやくSwift4/RxSwift4/iPhone Xの対応が落ち着いたところです。
このブログでは、10月11日に開催した Souzoh iOS Talkの中で発表した
メルカリ アッテを支えるオートマトンについて、より詳細な内容をお伝えします。
当日のスライドは下記になります。
会員登録フロー
会員登録フローを実装する時に、以下のような基本的な機能を実装することがあります。
- メールアドレスとパスワードで登録できること
- メールアドレスとパスワードでログインできること
- Facebookで認証(登録、ログイン)できること
- パスワード再発行できること
さらに、メルカリ アッテのようにセカンドパーティーのアプリなら、メルカリ連携の実装も必要になります。
- メルカリのお客様であればワンクリックでアッテ登録/ログインできること
アッテアプリ内にはメルカリトークンと アッテトークンという2種類の秘匿情報があり、トークンは変わったり、リフレッシュして再取得したりする仕組みがあります。
メルカリアプリ利用中のお客様はメルカリトークン、アッテアプリ利用中のお客様は両方を持っていますが、
仮にアッテをログアウトしてもトークンは残ります。
アッテアプリを起動する時に、既にどのトークンがあるかないかによって新規登録/メルカリ連携/アッテワンクリックログインの3択を出さなければなりません。
ではFacebook認証を考えましょう。アッテアプリを起動する時にトークンを持っていないお客様は新規のお客様とみなしていいでしょうか?
答えは、そのデバイスだけ使っていれば正です。
iPhone買い替えてアプリを新しくダウンロードすると新規のお客様ではありませんし、AndroidからiPhoneに乗り換えする場合も一緒です。そんな時に新iPhoneは当然トークンがないですが、お客様がFacebook認証したらそのFacebookアカウントに連携があった前のデバイスの情報に基づいて3択を出さなければなりません。
さらにログイン画面を考えましょう。ログイン画面にてメールアドレスとパスワードを入力すると、同じくそのアカウントが前のデバイスでの連携があるかないかで3択を出さなければなりませんし、ログイン画面にもFacebookボタンがあり、そのボタンを押すと同じく上記で述べたフローに通ります。
遷移ロジックはドメイン知識
フローが複雑になるとともに、その遷移グラフをうまく管理できないと変更が発生した時や考慮漏れした時に対応コストが大きくなります。遷移ロジックはドメイン知識なのでModel層に置くべきだし、テストできるべきです。また画面毎のViewControllerのDependenciesも頻繁に変わるとMove Fastな開発はできない状態になります。
ここでオートマトンの技術、いわゆるステートマシン導入する話になります。
ステートマシンの遷移関数
ステートマシンを簡単に言えば状態(ステート)をうまく管理できる仕組みです。
フロントエンド開発にはRedux, Elmなどいくつか有名なアーキテクチャーがありますが、内部的にはステートマシン技術です。
オートマトン理論には有限オートマトン、Mealy Machine, Moore Machine, Turing Machineなど色々な種類のオートマトンが存在しますが、それぞれの遷移関数(Reducer)が違って、旧状態から新状態に切り替える時の入力、出力の関係から分類されます。下記の遷移関数の中にs
が状態、a
が入力、b
が出力になります。
// 有限オートマトン (s, a) -> s // Mealy Machine (s, a) -> s (s, a) -> b // Moore Machine (s, a) -> s s -> b
上記の遷移関数をまとめてカリー化すると、Mealy MachineはElmのアーキテクチャーに似てることに気付けると思います。
さらにカリー化すると、状態s
から「状態付きの計算」/ステートモナドへの関数に表現できます。
// Elm fun update(state: State, action: Action): Pair<State, Command> // Mealy Machine (s, a) -> (s, b) // aka a -> s -> (s, b) // aka a -> State[s, b]
最後のa -> State[s, b]
の遷移関数を持つオートマトンを実装するには数行でできます
class MonadicAutomaton<S, A, B> { typealias T = (A) -> State<S, B> private var f : T init(f: @escaping T) { self.f = f } func transition(from: S, by: A) -> (S, B) { return f(by).run(s: from) } }
遷移のイベント
上記のインプット型A
はイベント型です。
旧状態から新状態へ変わるきっかけのイベント型を付属型enumに定義し、遷移に必要な情報、主に次のViewControllerのDependency、を付属値に持たせることができます。
ここで余談ですがViewControllerのDependencyを明確化するのはメルカリ アッテの一つのポリシーであり、すべてのSegue遷移を廃棄した上で遷移先のViewControllerをDependencyInjectableプロトコルに準拠させています。
protocol DependencyInjectable { associatedtype Dependency = Void static func make(withDependency dependency: Dependency) -> Self } final class AtteLoginViewController: UIViewController, DependencyInjectable { struct Dependency { let resource: MercariIdResource let token: String } private var resource: MercariIdResource! private var token: String! static func make(withDependency dependency: Dependency) -> AtteLoginViewController { let vc = AtteLoginViewController() vc.resource = dependency.resource vc.token = dependency.token return vc } }
遷移関数の話に戻しますが、ここのアウトプットB
は何に相当するのでしょう?
UI変更はステートマシンの副作用
ステートマシンがゴロゴロ状態変更する間に生まれる副作用のほとんどはUI変更です。
今回アプリ全体のステートマシンを実現しているので、そのUI変更は丸ごとViewController、いわゆるB
= UIViewController
だと考えればいいでしょう。
逆に一つの画面内にステートマシンを実現する時の副作用は細かいUIコンポネントの変更(ボタンの表示/非表示、背景色変更等)になります。
RxSwiftを使っているなら B
= バインドしたDisposables
型だと思います。
UIViewController
をアウトプット型に指定して下記のように遷移関数を書けます:
let transitionFunc: (REvent) -> State<RState, UIViewController> = { event in switch event { case .registerWithFacebook(let profile): let vc = ProfileRegisterViewController.make(withDependency: .init(facebookProfile: profile)) return State<RState, UIViewController> { s in let s1: RState = s == .registerRoot ? .profileRegister : .any return (s1, vc) //... } } let registrationMachine = MonadicAutomaton<RState, REvent, UIViewController>(f : transitionFunc)
このロジックのテストはViewの実装に関係なく下記のように書けます。
context("SMS未認証でメルカリ登録したがアッテ未登録の場合") { it("SMS認証を行なってアッテ登録できる") { self.tryRoute(events: [ FixtureEvent.showRegisterRoot, FixtureEvent.startWithMercari, FixtureEvent.registerWithMercariResource, FixtureEvent.confirmSMS, FixtureEvent.sendSMSConfirmationNumber, FixtureEvent.loginAndShowHome ]) XCTAssert(self.currentState == .home) } }
まとめ
状態管理技術のステートマシンを活用することにより、アプリ会員登録フローの実装から遷移ロジックだけModel層に切り出すことができ、容易にメンテナブルとテスタブルなコードを書くことが出来ました。
さらにViewControllerのDependencyを明確化した上で、UIViewController
自体をステートマシンの副作用として扱うと効率的に遷移グラフを定義できました。
フロントエンド開発の中にやはりステートマシン技術が重要だと思います。今回紹介したステートマシンはアプリ全体のビジネスロジックを管理するステートマシンですが、一つの画面の中にUI要素の変更を管理するステートマシンもあります。
今年のtry! Swift NYCに発表があったRxSwift作者である@kzaherさんのRxFeedBackが代表的です。
RxFeedbackのReducerはまだswitch
を使用していますが、Reducerをそのままscan
オペレーターに渡し、ステートとイベントをそれぞれストリーム型に表現しながら変換していく仕組みが面白いです。
ただしRxFeedBackはMVVMアーキテクチャーではないので、現時点でRxSwift + MVVMを使っている場合はすこし応用しづらいと思います。
参考
詳しくコードをご覧になりたい方は最初のスライドか下記のサンプルコードをご覧ください。
RxFeedBackの発表資料
遷移関数の組み立て可能性(composability)をさらに活用するコンセプトとして、Functional Swift Conferenceに発表があったComposable Reducers & Effects Systemsをおすすめします。