状態設計から「なんとなく」を無くそう
by
Masaki Hara
Link
Embed
Share
Beginning
This slide
Copy link URL
Copy link URL
Copy iframe embed code
Copy iframe embed code
Copy javascript embed code
Copy javascript embed code
Share
Tweet
Share
Tweet
Slide 1
Slide 1 text
© 2023 Wantedly, Inc. 状態設計から 「なんとなく」を無くそう Wantedly Tech Lunch Dec. 7 2023 - Masaki Hara
Slide 2
Slide 2 text
© 2023 Wantedly, Inc. 目的 ● 状態の設計が悪いと、コーナーケースが発生しやすくなります ● コーナーケースはプロダクトの成長にともない顕在化し、ユー ザーとサポートと開発者に牙を剥きます。 ● 状態を合理的に設計することで、問題を未然に抑止しましょ う。
Slide 3
Slide 3 text
© 2023 Wantedly, Inc. 抽象的に考える ● 本発表では「状態」という概念をあえて抽象的に扱います ○ 具体例も出しますが、具体例に引っぱられすぎないように注意してください。 ● 異なる粒度の問題に普遍的に使える考え方を提示することを 試みます ○ 古典力学は惑星運動と地上の力学を同時に説明することに成功しました。このような普 遍性を提供することがここでの目標です。
Slide 4
Slide 4 text
© 2023 Wantedly, Inc. 枠組み 以下の3点を意識的に決定しよう ● 状態空間 ● 状態の置き場所 ● 状態の表現
Slide 5
Slide 5 text
© 2023 Wantedly, Inc. 1: 状態空間
Slide 6
Slide 6 text
© 2023 Wantedly, Inc. 枠組み 以下の3点を意識的に決定しよう ● 状態空間 ● 状態の置き場所 ● 状態の表現
Slide 7
Slide 7 text
© 2023 Wantedly, Inc. 状態空間 「状態空間」をここでは以下の (Q, Σ, δ)とする: ● 状態集合 Q ● 入力の集合 Σ ● 入力の作用 δ: Q × Σ → Q 例を見てみよう→
Slide 8
Slide 8 text
© 2023 Wantedly, Inc. 小さい状態空間: いいねの状態 ● 状態集合 Q = { 未いいね, いいね済 } ● 入力の集合 Σ = { いいねする, いいねを外す } ● 入力の作用 δ: 未いいね いいね済 いいねを外す いいねを外す いいねする いいねする
Slide 9
Slide 9 text
© 2023 Wantedly, Inc. 大きい状態空間: SQL ● 状態集合 Q = { テーブルの全状態 } ● 入力の集合 Σ = { 全てのSQL DMLコマンド } ● 入力の作用 δ(q, c) = (cの実行後のテーブル)
Slide 10
Slide 10 text
© 2023 Wantedly, Inc. 大きい状態空間: Redux ● 状態集合 Q = State ● 入力の集合 Σ = Action ● 入力の作用 δ ∈ Reducer
Slide 11
Slide 11 text
© 2023 Wantedly, Inc. 4つのアノマリー ● 状態空間の代表的な4つのアノマリーを紹介 ● アノマリーがないように状態を作ってみよう ※ これらのアノマリーにより、前のスライドの定義を満たさないものも出てきます
Slide 12
Slide 12 text
© 2023 Wantedly, Inc. 加法的なアノマリー + - 「到達不能」アノマリー ● 余分な状態 ● 状態を消して対応 「状態不足」アノマリー ? ● 状態が足りない ● 状態を足して対応
Slide 13
Slide 13 text
© 2023 Wantedly, Inc. 乗法的なアノマリー × ÷ 「重複」アノマリー ● 区別する必要がない状態 ● 状態を統合して対応 「情報不足」アノマリー ● 遷移先が決定できない ● 状態を分割して対応 ?
Slide 14
Slide 14 text
© 2023 Wantedly, Inc. 情報の消失 ● 同じ入力で同じ状態に合流するパターンにも注意 ● 情報が捨てられていることで起きる何らかの問題を示唆して いる場合がある
Slide 15
Slide 15 text
© 2023 Wantedly, Inc. 例: 相互フォローシステム ● フォロー機能のあるSNSを考える ● フォローすると同時にフォローリクエストが飛ぶ ● フォローリクエストは受諾または無視できる
Slide 16
Slide 16 text
© 2023 Wantedly, Inc. 例: 相互フォローシステム 未フォロー フォロー 被フォロー フォロー +リクエスト 被フォロー +リクエスト 相互フォロー
Slide 17
Slide 17 text
© 2023 Wantedly, Inc. 例: 相互フォローシステム 未フォロー フォロー 被フォロー フォロー +リクエスト 被フォロー +リクエスト 相互フォロー フォロー 被受諾 被無視 被フォロー 受諾 無視 被フォロー フォロー
Slide 18
Slide 18 text
© 2023 Wantedly, Inc. 例: 相互フォローシステム 未フォロー フォロー 被フォロー フォロー +リクエスト 被フォロー +リクエスト 相互フォロー アンフォロー アンフォロー 被アンフォロー 被アンフォロー アンフォロー
Slide 19
Slide 19 text
© 2023 Wantedly, Inc. 問題 ● この「相互フォロー」システムには何か問題がありますか?
Slide 20
Slide 20 text
© 2023 Wantedly, Inc. 解答例: State Smellの発見 ● 以下で状態の合流 (=情報の消失) が起きている 未フォロー フォロー フォロー +リクエスト アンフォロー アンフォロー
Slide 21
Slide 21 text
© 2023 Wantedly, Inc. 解答例: State Smellの発見 ● →情報が消失したあとの遷移に注目する 未フォロー フォロー フォロー +リクエスト アンフォロー アンフォロー フォロー +リクエスト フォロー
Slide 22
Slide 22 text
© 2023 Wantedly, Inc. 解答例: State Smellの発見 ● →無視されてもアンフォロー&フォローで復活できる 未フォロー フォロー アンフォロー フォロー +リクエスト フォロー 被無視
Slide 23
Slide 23 text
© 2023 Wantedly, Inc. 解答例 ● フォローリクエストの「無視」を突き抜ける戦略があることがわ かった ○ ただし、これが実際に問題かどうかはプロダクト判断になる。 ○ 大事なのは、意図をもって判断を下すことができるようになること ● では、直すとしたら?
Slide 24
Slide 24 text
© 2023 Wantedly, Inc. 解答例: State Smellの解消 ● やりたいこと: 「フォロー」の結果を変えたい 未フォロー フォロー フォロー +リクエスト アンフォロー アンフォロー フォロー +リクエスト フォロー フォロー フォロー
Slide 25
Slide 25 text
© 2023 Wantedly, Inc. 解答例: State Smellの解消 ● → 「情報不足」アノマリーが起きている 未フォロー フォロー フォロー +リクエスト アンフォロー アンフォロー フォロー +リクエスト フォロー フォロー フォロー
Slide 26
Slide 26 text
© 2023 Wantedly, Inc. 解答例: State Smellの解消 ● 状態を分割して対応 未フォロー フォロー フォロー +リクエスト アンフォロー アンフォロー フォロー +リクエスト フォロー フォロー フォロー 未フォロー +被無視
Slide 27
Slide 27 text
© 2023 Wantedly, Inc. モデルと向き合うときに大事なこと ● モデルを観察して、怪しい箇所を探す ○ これはある程度形式的にできる ● → 具体例に落としこんで、プロダクトオーナーの視点で考え直 す ○ これはモデルの外で起きる ○ プロダクトオーナー自身でなくてもある程度はやってみよう
Slide 28
Slide 28 text
© 2023 Wantedly, Inc. 2: 状態の置き場所
Slide 29
Slide 29 text
© 2023 Wantedly, Inc. 枠組み 以下の3点を意識的に決定しよう ● 状態空間 ● 状態の置き場所 ● 状態の表現
Slide 30
Slide 30 text
© 2023 Wantedly, Inc. 状態ストア 状態には保存する場所が必要 → 状態ストア の概念
Slide 31
Slide 31 text
© 2023 Wantedly, Inc. 状態ツリー 大きい状態 = 小さい状態の組み合わせ RealWorld Server Client Human RDB Redis Cookie Location Memory Users Posts Session Cache Auth
Slide 32
Slide 32 text
© 2023 Wantedly, Inc. 状態ツリー ● 小さい状態の置き場を考えること = 大きい状態の状態空間設計 ● → 大きい状態をモデリングすることで同じ考え方が適用でき る
Slide 33
Slide 33 text
© 2023 Wantedly, Inc. 状態置き場を考えるときの原則 ● 原則1: 異なる状態ストアは異なるライフサイクルを持つ ● 原則2: ストア間の同期を信用してはいけない
Slide 34
Slide 34 text
© 2023 Wantedly, Inc. 2-1: 状態ストアのライフサイクル
Slide 35
Slide 35 text
© 2023 Wantedly, Inc. 原則1 異なる状態ストアは異なるライフサイクルを持つ → そのストアが「いつ初期化され」「いつ消去されるのか」を意識 して選択しよう
Slide 36
Slide 36 text
© 2023 Wantedly, Inc. 原則1 例: ダークモード ● Webサイトでダークモードとライトモードを選択できるようにし たい。 ● 状態をどこに保存するか?
Slide 37
Slide 37 text
© 2023 Wantedly, Inc. 原則1 ● サーバー側 (ユーザー設定): ○ ユーザーが作られたときに初期化され、退会によって消去される。 ○ → 匿名の場合を考慮する必要がある ● ブラウザ localStorage/Persistent Cookie: ○ 新しいブラウザでアクセスしたときに初期化される。 ○ → 同じユーザーでも、複数のブラウザを利用していれば別の値が入る。 ● ブラウザ sessionStorage/Session Cookie: ○ ブラウザ起動時に初期化され、終了時に消去される。 ○ → ブラウザを再起動したときに永続化されない
Slide 38
Slide 38 text
© 2023 Wantedly, Inc. 原則1 ● URL (query / fragment) ○ URL発行によって初期化される。 ○ → URLを別のユーザーに共有しても永続化される。 ○ → 逆に、別タブにその影響は漏れ出さない。 ● History data ○ URLと近いが、URL共有で引き継がれない。
Slide 39
Slide 39 text
© 2023 Wantedly, Inc. 例題 以下が発生するシナリオを考えてください。 ● ユーザー設定よりもlocalStorageが長生きする場合 ● localStorageよりもHistory dataが長生きする場合
Slide 40
Slide 40 text
© 2023 Wantedly, Inc. 解答例 解答例 ● 同じブラウザでログアウトして別のユーザーで再ログインした 場合 ○ ログアウト時にlocalStorageを消さない場合。消す場合でも、 localStorageを devtoolsでコピーするなどのシナリオは考えられる ● history.pushで別ページに遷移して設定を変更後、ブラウザ バックで元のページに戻った場合
Slide 41
Slide 41 text
© 2023 Wantedly, Inc. 具体例から最適解を考える ● 差異が生じる具体的なケースがイメージできれば、プロダクト オーナー視点で判断ができるはず ● たとえば…… ○ ユーザーが自分の好みを持ち運べるようにしたい ? → サーバーがいいかも ○ デバイスごとに事情が違うかも? → localStorageがいいかも ○ どちらかが常に正解というわけではない。 大事なのは、意図をもって判断を下すことができるようになること
Slide 42
Slide 42 text
© 2023 Wantedly, Inc. 2-2: 状態ストアの同期
Slide 43
Slide 43 text
© 2023 Wantedly, Inc. 原則2 原則2: ストア間の同期を信用しない → 絶対に守りたい相関があるなら、同じストアに配置する さもなくば既存のトランザクションを使うか、 頑張ってトランザクションを組む (茨の道)
Slide 44
Slide 44 text
© 2023 Wantedly, Inc. 隠れ状態 隠れ状態 ● 複数のストアが正しく同期されている間は起きない状態 ● コマンドが一部のストアにだけ適用されることで起きる 本発表における「状態空間」の数学的な定義に基づいて説明するなら : 状態の直積として入力 Σが共通 (Σ = Σ 1 = Σ 2 ) なものを考えていたが、入力が独立に行われるような 直積 (Σ = Σ 1 ⨿ Σ 2 ) を考えることもできる。このような直積では、元の状態空間で到達可能な状態の 組み合わせであれば直積空間でも到達可能であるといえる。
Slide 45
Slide 45 text
© 2023 Wantedly, Inc. 例 例(再掲): 相互フォローシステム (6状態)
Slide 46
Slide 46 text
© 2023 Wantedly, Inc. 例: 相互フォローシステム 未フォロー フォロー 被フォロー フォロー +リクエスト 被フォロー +リクエスト 相互フォロー フォロー 被受諾 被無視 被フォロー 受諾 無視 被フォロー フォロー
Slide 47
Slide 47 text
© 2023 Wantedly, Inc. 例: 相互フォローシステム 未フォロー フォロー 被フォロー フォロー +リクエスト 被フォロー +リクエスト 相互フォロー アンフォロー アンフォロー 被アンフォロー 被アンフォロー アンフォロー
Slide 48
Slide 48 text
© 2023 Wantedly, Inc. 相互フォローシステムの状態分割 ● この状態をDBで表現するときは、フォロー状態と被フォロー状 態は分けて管理するのが一般的 ○ リクエストの持ち方に任意性があるが、ここでは被リクエスト側の状態として持つことを考 える ● この場合はストア = グラフ上のエントリとなる ○ RDBでいえば1つの行に注目している
Slide 49
Slide 49 text
© 2023 Wantedly, Inc. 相互フォローシステムの状態分割 片側の状態 未フォロー 被リクエスト (未フォロー) フォロー 被フォロー フォロー 受諾 アンフォロー 被アンフォロー or 無視
Slide 50
Slide 50 text
© 2023 Wantedly, Inc. 相互フォローシステムの状態分割 ● 自分側3状態 × 相手側3状態 = 9状態 ● 実際に到達できるのは6状態なので、元の状態空間と同じに なる → これが仮定できるかどうかは場合による
Slide 51
Slide 51 text
© 2023 Wantedly, Inc. 相互フォローシステムの状態分割 ● 6状態しかないと言えるのは、両ストアへの作用がアトミックに 行われると仮定しているから ○ まっとうな分離レベルのトランザクションであれば問題ない ● アトミックでない場合は変な状態が発生しうる
Slide 52
Slide 52 text
© 2023 Wantedly, Inc. 相互フォローシステムの状態分割 例: 双方が同時にフォローを行った場合のシナリオ ● A側のフォロー処理は以下の2手順 ○ A→Bの状態を「フォロー」にする ○ B→Aの状態を「被リクエスト」にする ● B側も同様 ● 両者の手順が交互に行われると……? ○ 両方向の状態が「被リクエスト」になってしまう
Slide 53
Slide 53 text
© 2023 Wantedly, Inc. 隠れ状態 ● 隠れ状態: 状態空間の直積のうち、同期が取れている限りは 到達しないはずの状態 ○ A→Bが「被リクエスト」 なのに B→Aが「未フォロー」 ○ A→Bが「未フォロー」 なのに B→Aが「被リクエスト」 ○ A→Bが「被リクエスト」 なのに B→Aも「被リクエスト」 ● アトミックでない場合、実際は到達可能なことが多い
Slide 54
Slide 54 text
© 2023 Wantedly, Inc. 同期を信用しないという選択 ● 全ての同期が完全である必要はない ○ 結果整合で十分な場合などはある ● 同期が完全でない場合 → 隠れ状態にあっても大丈夫なよう に実装する ○ 「この組み合わせはない」という仮定を置かないように実装しよう
Slide 55
Slide 55 text
© 2023 Wantedly, Inc. 2-2’: キャッシュ
Slide 56
Slide 56 text
© 2023 Wantedly, Inc. キャッシュ ● キャッシュ = あえて冗長に、複数の状態ストアに同じ情報を持 たせること ○ たとえば、サーバーにある状態をクライアントの状態としてコピーする ● 一定の不整合を許容する見返りにメリットが得られる ○ パフォーマンス ○ ユーザー体験のチューニング 例: ブックマークした投稿の一覧からブックマークを外したとき、一覧から即座に消えない ようにする
Slide 57
Slide 57 text
© 2023 Wantedly, Inc. キャッシュ ● 意図しないキャッシュ構造ができている場合もある ○ React.useStateを使ったが、実は直接取得可能な状態だった ● ストアを作るとき、それが冗長でないかよく考えよう
Slide 58
Slide 58 text
© 2023 Wantedly, Inc. 3: 状態の表現
Slide 59
Slide 59 text
© 2023 Wantedly, Inc. 枠組み 以下の3点を意識的に決定しよう ● 状態空間 ● 状態の置き場所 ● 状態の表現
Slide 60
Slide 60 text
© 2023 Wantedly, Inc. 状態の表現 ● 状態集合をぴったり表現できるとは限らない ● 「状態の表現の集合」 ≠ 「状態集合」
Slide 61
Slide 61 text
© 2023 Wantedly, Inc. 状態の表現 「状態の表現の集合」と「状態集合」のギャップは以下の2種類 ● 除外 (部分集合) … 実際には出現しない表現がある。 ● 同一視 (商集合) … 複数の表現をもつ状態がある。 いずれのギャップも少ないほうがよい - ÷
Slide 62
Slide 62 text
© 2023 Wantedly, Inc. 非正規形 ● 非正規形 = 除外によっても同一視によっても対応できる表現 ○ 「隠れ状態」は非正規形 ● 例: boolをintで表現する ○ 0, 1: 正規形 ○ 2以上の整数: 原則として除外されるが、もし存在した場合は 1と同一視される
Slide 63
Slide 63 text
© 2023 Wantedly, Inc. ギャップは悪か? ● 表現の工夫によって保証することが難しい性質もある ○ 例: 「プロフィールの登録が完了していなければコンテンツを投稿できない」 ● 状態集合とその表現のギャップを受け入れることも必要 ○ 大事なのは、意図をもって判断を下すことができるようになること
Slide 64
Slide 64 text
© 2023 Wantedly, Inc. まとめ
Slide 65
Slide 65 text
© 2023 Wantedly, Inc. まとめ ● 小さな状態、大きな状態の両方に目を向けてみよう ● 状態空間そのもの、その置き場所、そして表現の3つに分けて 考えてみよう ● 状態管理のコーナーケースを具体例に落としこみ、プロダクト のあり方に翻って合理的な判断をしよう ○ 大事なのは、意図をもって判断を下すことができるようになること