Upgrade to Pro — share decks privately, control downloads, hide ads and more …

状態設計から「なんとなく」を無くそう

Sponsored · Ship Features Fearlessly Turn features on and off without deploys. Used by thousands of Ruby developers.
Avatar for Masaki Hara Masaki Hara
December 07, 2023

 状態設計から「なんとなく」を無くそう

ウォンテッドリー株式会社の社内イベント "Tech Lunch" で話した発表です。

プログラムには大小さまざまな粒度の「状態」が存在します。
状態の設計を工夫することで、コーナーケースの発生を抑止し、ユーザー体験を最適化することができます。
本発表では、私が普段どのように「状態」について考えているか、言語や環境を問わずできるだけ普遍的に使える形での言語化を試みます。本発表を通じて、「状態」をなんとなくではなく合理的に設計するためのヒントを提供します。

GoogleスライドのURL: https://docs.google.com/presentation/d/1PNzz69UV05HlKPuWGlooemnPslLbLKsyLwl3R4U_XqE/edit

Avatar for Masaki Hara

Masaki Hara

December 07, 2023
Tweet

More Decks by Masaki Hara

Other Decks in Programming

Transcript

  1. © 2023 Wantedly, Inc. 抽象的に考える • 本発表では「状態」という概念をあえて抽象的に扱います ◦ 具体例も出しますが、具体例に引っぱられすぎないように注意してください。 •

    異なる粒度の問題に普遍的に使える考え方を提示することを 試みます ◦ 古典力学は惑星運動と地上の力学を同時に説明することに成功しました。このような普 遍性を提供することがここでの目標です。
  2. © 2023 Wantedly, Inc. 状態空間 「状態空間」をここでは以下の (Q, Σ, δ)とする: •

    状態集合 Q • 入力の集合 Σ • 入力の作用 δ: Q × Σ → Q 例を見てみよう→
  3. © 2023 Wantedly, Inc. 小さい状態空間: いいねの状態 • 状態集合 Q =

    { 未いいね, いいね済 } • 入力の集合 Σ = { いいねする, いいねを外す } • 入力の作用 δ: 未いいね いいね済 いいねを外す いいねを外す いいねする いいねする
  4. © 2023 Wantedly, Inc. 大きい状態空間: SQL • 状態集合 Q =

    { テーブルの全状態 } • 入力の集合 Σ = { 全てのSQL DMLコマンド } • 入力の作用 δ(q, c) = (cの実行後のテーブル)
  5. © 2023 Wantedly, Inc. 大きい状態空間: Redux • 状態集合 Q =

    State • 入力の集合 Σ = Action • 入力の作用 δ ∈ Reducer
  6. © 2023 Wantedly, Inc. 加法的なアノマリー + - 「到達不能」アノマリー • 余分な状態

    • 状態を消して対応 「状態不足」アノマリー ? • 状態が足りない • 状態を足して対応
  7. © 2023 Wantedly, Inc. 乗法的なアノマリー × ÷ 「重複」アノマリー • 区別する必要がない状態

    • 状態を統合して対応 「情報不足」アノマリー • 遷移先が決定できない • 状態を分割して対応 ?
  8. © 2023 Wantedly, Inc. 例: 相互フォローシステム 未フォロー フォロー 被フォロー フォロー

    +リクエスト 被フォロー +リクエスト 相互フォロー フォロー 被受諾 被無視 被フォロー 受諾 無視 被フォロー フォロー
  9. © 2023 Wantedly, Inc. 例: 相互フォローシステム 未フォロー フォロー 被フォロー フォロー

    +リクエスト 被フォロー +リクエスト 相互フォロー アンフォロー アンフォロー 被アンフォロー 被アンフォロー アンフォロー
  10. © 2023 Wantedly, Inc. 解答例: State Smellの発見 • 以下で状態の合流 (=情報の消失)

    が起きている 未フォロー フォロー フォロー +リクエスト アンフォロー アンフォロー
  11. © 2023 Wantedly, Inc. 解答例: State Smellの発見 • →情報が消失したあとの遷移に注目する 未フォロー

    フォロー フォロー +リクエスト アンフォロー アンフォロー フォロー +リクエスト フォロー
  12. © 2023 Wantedly, Inc. 解答例: State Smellの解消 • やりたいこと: 「フォロー」の結果を変えたい

    未フォロー フォロー フォロー +リクエスト アンフォロー アンフォロー フォロー +リクエスト フォロー フォロー フォロー
  13. © 2023 Wantedly, Inc. 解答例: State Smellの解消 • → 「情報不足」アノマリーが起きている

    未フォロー フォロー フォロー +リクエスト アンフォロー アンフォロー フォロー +リクエスト フォロー フォロー フォロー
  14. © 2023 Wantedly, Inc. 解答例: State Smellの解消 • 状態を分割して対応 未フォロー

    フォロー フォロー +リクエスト アンフォロー アンフォロー フォロー +リクエスト フォロー フォロー フォロー 未フォロー +被無視
  15. © 2023 Wantedly, Inc. モデルと向き合うときに大事なこと • モデルを観察して、怪しい箇所を探す ◦ これはある程度形式的にできる •

    → 具体例に落としこんで、プロダクトオーナーの視点で考え直 す ◦ これはモデルの外で起きる ◦ プロダクトオーナー自身でなくてもある程度はやってみよう
  16. © 2023 Wantedly, Inc. 状態ツリー 大きい状態 = 小さい状態の組み合わせ RealWorld Server

    Client Human RDB Redis Cookie Location Memory Users Posts Session Cache Auth
  17. © 2023 Wantedly, Inc. 状態ツリー • 小さい状態の置き場を考えること = 大きい状態の状態空間設計 •

    → 大きい状態をモデリングすることで同じ考え方が適用でき る
  18. © 2023 Wantedly, Inc. 原則1 • サーバー側 (ユーザー設定): ◦ ユーザーが作られたときに初期化され、退会によって消去される。

    ◦ → 匿名の場合を考慮する必要がある • ブラウザ localStorage/Persistent Cookie: ◦ 新しいブラウザでアクセスしたときに初期化される。 ◦ → 同じユーザーでも、複数のブラウザを利用していれば別の値が入る。 • ブラウザ sessionStorage/Session Cookie: ◦ ブラウザ起動時に初期化され、終了時に消去される。 ◦ → ブラウザを再起動したときに永続化されない
  19. © 2023 Wantedly, Inc. 原則1 • URL (query / fragment)

    ◦ URL発行によって初期化される。 ◦ → URLを別のユーザーに共有しても永続化される。 ◦ → 逆に、別タブにその影響は漏れ出さない。 • History data ◦ URLと近いが、URL共有で引き継がれない。
  20. © 2023 Wantedly, Inc. 解答例 解答例 • 同じブラウザでログアウトして別のユーザーで再ログインした 場合 ◦

    ログアウト時にlocalStorageを消さない場合。消す場合でも、 localStorageを devtoolsでコピーするなどのシナリオは考えられる • history.pushで別ページに遷移して設定を変更後、ブラウザ バックで元のページに戻った場合
  21. © 2023 Wantedly, Inc. 具体例から最適解を考える • 差異が生じる具体的なケースがイメージできれば、プロダクト オーナー視点で判断ができるはず • たとえば……

    ◦ ユーザーが自分の好みを持ち運べるようにしたい ? → サーバーがいいかも ◦ デバイスごとに事情が違うかも? → localStorageがいいかも ◦ どちらかが常に正解というわけではない。 大事なのは、意図をもって判断を下すことができるようになること
  22. © 2023 Wantedly, Inc. 隠れ状態 隠れ状態 • 複数のストアが正しく同期されている間は起きない状態 • コマンドが一部のストアにだけ適用されることで起きる

    本発表における「状態空間」の数学的な定義に基づいて説明するなら : 状態の直積として入力 Σが共通 (Σ = Σ 1 = Σ 2 ) なものを考えていたが、入力が独立に行われるような 直積 (Σ = Σ 1 ⨿ Σ 2 ) を考えることもできる。このような直積では、元の状態空間で到達可能な状態の 組み合わせであれば直積空間でも到達可能であるといえる。
  23. © 2023 Wantedly, Inc. 例: 相互フォローシステム 未フォロー フォロー 被フォロー フォロー

    +リクエスト 被フォロー +リクエスト 相互フォロー フォロー 被受諾 被無視 被フォロー 受諾 無視 被フォロー フォロー
  24. © 2023 Wantedly, Inc. 例: 相互フォローシステム 未フォロー フォロー 被フォロー フォロー

    +リクエスト 被フォロー +リクエスト 相互フォロー アンフォロー アンフォロー 被アンフォロー 被アンフォロー アンフォロー
  25. © 2023 Wantedly, Inc. 相互フォローシステムの状態分割 • 自分側3状態 × 相手側3状態 =

    9状態 • 実際に到達できるのは6状態なので、元の状態空間と同じに なる → これが仮定できるかどうかは場合による
  26. © 2023 Wantedly, Inc. 相互フォローシステムの状態分割 例: 双方が同時にフォローを行った場合のシナリオ • A側のフォロー処理は以下の2手順 ◦

    A→Bの状態を「フォロー」にする ◦ B→Aの状態を「被リクエスト」にする • B側も同様 • 両者の手順が交互に行われると……? ◦ 両方向の状態が「被リクエスト」になってしまう
  27. © 2023 Wantedly, Inc. 隠れ状態 • 隠れ状態: 状態空間の直積のうち、同期が取れている限りは 到達しないはずの状態 ◦

    A→Bが「被リクエスト」 なのに B→Aが「未フォロー」 ◦ A→Bが「未フォロー」 なのに B→Aが「被リクエスト」 ◦ A→Bが「被リクエスト」 なのに B→Aも「被リクエスト」 • アトミックでない場合、実際は到達可能なことが多い
  28. © 2023 Wantedly, Inc. 同期を信用しないという選択 • 全ての同期が完全である必要はない ◦ 結果整合で十分な場合などはある •

    同期が完全でない場合 → 隠れ状態にあっても大丈夫なよう に実装する ◦ 「この組み合わせはない」という仮定を置かないように実装しよう
  29. © 2023 Wantedly, Inc. キャッシュ • キャッシュ = あえて冗長に、複数の状態ストアに同じ情報を持 たせること

    ◦ たとえば、サーバーにある状態をクライアントの状態としてコピーする • 一定の不整合を許容する見返りにメリットが得られる ◦ パフォーマンス ◦ ユーザー体験のチューニング 例: ブックマークした投稿の一覧からブックマークを外したとき、一覧から即座に消えない ようにする
  30. © 2023 Wantedly, Inc. 状態の表現 「状態の表現の集合」と「状態集合」のギャップは以下の2種類 •  除外 (部分集合) …

    実際には出現しない表現がある。 •  同一視 (商集合) … 複数の表現をもつ状態がある。 いずれのギャップも少ないほうがよい - ÷
  31. © 2023 Wantedly, Inc. 非正規形 • 非正規形 = 除外によっても同一視によっても対応できる表現 ◦

    「隠れ状態」は非正規形 • 例: boolをintで表現する ◦ 0, 1: 正規形 ◦ 2以上の整数: 原則として除外されるが、もし存在した場合は 1と同一視される
  32. © 2023 Wantedly, Inc. ギャップは悪か? • 表現の工夫によって保証することが難しい性質もある ◦ 例: 「プロフィールの登録が完了していなければコンテンツを投稿できない」

    • 状態集合とその表現のギャップを受け入れることも必要 ◦ 大事なのは、意図をもって判断を下すことができるようになること
  33. © 2023 Wantedly, Inc. まとめ • 小さな状態、大きな状態の両方に目を向けてみよう • 状態空間そのもの、その置き場所、そして表現の3つに分けて 考えてみよう

    • 状態管理のコーナーケースを具体例に落としこみ、プロダクト のあり方に翻って合理的な判断をしよう ◦ 大事なのは、意図をもって判断を下すことができるようになること