たれぱんのびぼーろく

わたしの備忘録、生物学とプログラミングが多いかも

Reduxの哲学@2021

Non-opinionatedだけど、たくさんたくさーんのガイドラインとユーティリティがある.
理解した上でガイドラインから外れることを推奨している.

Store分割

ガイドライン: sliceに閉じた処理を推奨.
なのでsliceを跨いだslice利用(reducerにfull stateを渡す等)をしない方を推奨.

"Why doesn't combineReducers include a third argument with the entire state when it calls each reducer?"

Action-Reducer対応

When a given action is dispatched, it might be handled by all, some, or none of the reducers.
Style Guide | Redux

#

medium.com

余談

ReduxはSSoTなので、グローバル状態を管理する.
どこからでもアクセスできるたった1つの状態 ~ グローバル変数
変更と利用が厳密に制御されている(dispatchとsubscribe)ので、グローバル変数にありがちな暗示的依存はない.

抽象化とYAGNI/KISS

その抽象化、本当に必要ですか/YAGNI
無駄に凝集度を下げてシンプルさを失ってませんか/KISS?

抽象化と疎結合

抽象への依存は疎結合を促進する。

  • origin 一体: AB
  • step1 分離: A---B
  • step2 抽象化: A-inter & inter-B => A.bind(B)

一体化してるやつを切り出し.
例えば関数にして切り出し.

切り出し部分にインターフェース定義 + 注入
「抽象に依存」しているので好きな具象を注入 (依存性の注入) できる.

インターフェースだけを繋ぎにして両側が独立.
AをA'にしても、BをB'にしてもOK.
両者を疎結合 (低い結合度、loose coupling) にする。

無限分割

1行1行にインターフェースを用意しようと思えば用意できる.

抽象化選手権

お題: タイマーでアラーム起動

素朴な実装

setAlerm = () => setTimeout(alert, 2*3600, "おはよー");

setAlerm();

TimerとReaction

type reaction = () => void  

setTimer = (rct: reaction) => setTimeout(rct, 2*3600);
hello : reaction = () => alert("おはよー");

setTimer(hello);

「起動」と「事象」の分離.
事象側はタイマーの実装を知らなくて良いし、起動側は事象の実装を知らなくて良い.
setTimer(goodNight)とか簡単に用意可能.
Timerの実装をsetTimeoutから差し替えるのも可能.

Trigger - Bridge - Reaction

「引き金」と「橋渡し」と「反応」の分離

type trigger = () => Promise<void, never>  
type reaction = () => void  

timer : trigger = () => new Promise(resolve => setTimeout(resolve, 2*3600);
hello : reaction = () => alert("おはよー");
bridgeActions = (trg: trigger, rct: reaction) => trg().then(rct);

await bridgeActions(timer, hello);

2つのアクションを連携、的な抽象化.
bridgeActions(customerEnter, hello)とかできる.

Trigger - Action / Reducer

Flux/Redux-way.
メッセージパッシング.
TriggerがActionを産み、受け手Reducerは反応したいActionが来たら勝手に応答する.

その抽象YAGNI

抽象化した概念にマッチする複雑性を持ってない場合、YAGNI.
alert関数をお試しで使うためにRedux-wayなメッセージングは過剰.

抽象は存在で、抽象化は動作.
得られる・失われる「特性」を語ってない.

抽象への操作→捨象されたものを忘れられる→単一責任・高凝集

無限分割の何が不適切なのか

役割をサブ役割に、と無限分割が可能.
各ピースは厳密に1つの役割しか持たず、かつ疎結合にされているので、モジュール性が非常に高い.
よい性質を持ってそうなのに、何が悪いのか.

抽象化のコスト: 認知負荷
1セットの実装には全レイヤーの抽象化把握と具象化が必要.
1回しか使われない抽象は利用コストが嵩む.

N:M => N*M通りの把握
N:1:M => (N+1) + (M+1) 通りの把握

抽象の理解にコストがかかる (1の部分)。代わりに組み合わせ爆発を防止できる (*の部分)。

abstX = {instA, instB, instC, ...}
色んな具象を、ある側面から、1つの抽象にまとめあげる.

粒度?

より高レベルな言語を使えば具象側がシンプルになる (実のところは言語処理系が実装でプログラムは抽象)
適切な粒度・凝集度は言語エコシステム含んだ総合的な物.
時代・分野によって違って当然.

ReduxToolkit/createSliceの哲学: KISS原則を大事に

Redux ToolkitのcreateSliceはちょっとReduxぽくない処理をしてる。その背景にある哲学は何か。

特徴: ActionとReducerの凝集/密結合

Reduxは「ActionとReducerはN:M対応、互いに疎結合」という哲学1,2.
しかしRTK.createSliceはCaseReducerからAction/ActionCreatorを自動生成する。
つまり「ActionとReducerは1:1対応」にしてしまう.
これはぱっと見、Reduxの哲学に反している(そう感じている人のコメント link1, 2, 3).

問題意識: 機能的凝集を疎結合にするな

Reduxを採用する規模の状態管理では、2種類のActionが現れる.
(ここでの命名は私)

  • cross-domain Action: 複数のdomain3にまたがるAction
    • Action:Reducer = N:M。Eventなイメージ
  • in-domain Action: 1つのdomainにのみ影響4
    • Action:Reducer = 1:1。RPCなイメージ
    • Action設計段階から処理(reducer)に1:1で結びついている

Redux哲学とin-domain Actionの関係が問題。
ActionとRecuder (イベントと処理) が設計から結びついている == 機能的に凝集している。
なので凝集してるものをRedux-wayでむりくり疎結合にするのは凝集度を下げちゃう過剰な抽象化、KISS原則の違反

Ducksパターンはこれに対する回答の1つ5

考慮点: cross-domainあってのRedux

全部がin-domain ActionならReduxいらない。
でもある程度の規模になるとcross-domainも混在してくる.
in-domain Actionをcross-domain Actionとして利用したい日も出てくる.
この混在をどうするか考慮が必要.

解決策: in-domainは凝集させてcross-domainは疎結合

Redux ToolkitのcreateSliceは両タイプのAction/Reducerを扱う.

  • in-domain
    • CaseReducerに基づいたAction/ActionCreator生成 6(機能的凝集)
  • cross-domain
    • SliceReducer自動生成時にcross-domain actionを受け入れる 7
      • こっちはActionCreatorを作らない == Actionは別存在 == 疎結合

Actionというメッセージパッシングの枠組みは保持(== Redux)。
その上でin-domainな処理はAction自動生成&Reducerと同じ位置に保持(Ducksライク8, 9, 10)にすることでActionを意識させない作りにする.
cross-domainは今までどおり使えるようし、自動生成のActionもcross-domainへ転用できるような作り (1:1に見えるけど1:Nに使える).
部分部分で必要な抽象度を制御してKISS原則を守り、usefulnessを上げた(「過剰な抽象化による大量のボイラープレート」の除去)。

Refs


  1. ‘One of the key concepts of Redux is that each slice reducer “owns” its slice of state, and that many slice reducers can independently respond to the same action type.’ Redux Toolkit

  2. “caveats to keep in mind: Actions are not exclusively limited to a single slice. Any part of the reducer logic can (and should!) respond to any dispatched action.” Redux Toolkit

  3. == slice == FluxStore

  4. “95% of the time, it’s only one reducer/actions pair that ever needs their associated actions.” Ducks

  5. “for these pieces to be bundled together in an isolated module that is self contained”

  6. “the point of createSlice is … automatically generate the actions and types so that they don’t have to.” Redux Toolkit issue

  7. “I do want createSlice to support … that includes listening for action types that aren’t defined by this slice.” Redux Toolkit issue

  8. ‘the result object is conceptually similar to a “Redux duck” code structure.’ Redux Toolkit

  9. ‘createSlice does work as a “ducks” generator just fine.’ Redux Toolkit issue

  10. ‘to some extent createSlice does edge kinda close to the “Redux modules” concept’ Redux Toolkit issue

宣言的になるまでの流れ

全体の一部を変えたい
=> 操作メソッドをcallして一部を差し替えよう
=> 差し替え前の状態を把握している必要がある (新しいElemをぶら下げたい => 何にぶら下げる?)
=> 操作列しかないので状態を頭の中に再構成するしかない (脳内で [procedure].reduce((state, procedure) => procedure(state), init_state))
=> Asyncが来た日には脳が弾ける (もはや (state, procedure) => procedure(state) が成立しない)
=> そうだ、テンプレートエンジンにしてしまえ
=> 宣言的UI & 状態管理

状態管理

何を状態とするか、どこに状態を置くか、誰が状態へ変更するか、いつ状態を読み取るか

Viewにも変数、モデルにも変数、コントローラーにも変数、みたいな.
誰でもread/write可、変数を直接触れる、フォーマッターメソッド経由で触ることもある.
状態の分割等はなし、各部分で必要な状態をもつ.

The chaos.

対策

  • source of truth: 状態のコピーを存在させない => 状態量が減る、同期問題が無くなる
  • immutable state: 状態の更新はオブジェクト全体の差し替え => (state, change) => change(state) が冪等に => replayなどなど
  • (UI)
    • 宣言的UI: stateとviewの関係を記述するだけ => View側に状態を持たせる必要がなくなる

イベント駆動のパターン

イベント駆動プログラミングの実装において、イベントやイベントループはしばしばプラットフォームから提供される(c.f. ウェブブラウザ).
一方でイベントハンドラの設計は規定されず実装者に一任される。イベント駆動プログラミングにはしばしば頻出するハンドラ設計パターンが存在する。

もっとも素朴なパターンは1つのイベントに1つの専用ハンドラを設定するパターンである。例えばClickイベントに対してonClickイベントハンドラ1つのみを登録する。ハンドラは対応するイベントが引数となることを前提としてイベントに対する全ての処理を記述する(≒ 同期的メッセージキュー)。

また1つのイベントに複数のハンドラを登録するパターンもある。その場合、2つの設計がありうる。

アクティビティベース

特定のイベントに応答して起こる個別の活動(アクティビティ)を記述するハンドラ。ハンドラは対応するイベントが引数となることを前提としてイベントに対する個別のアクティビティを記述する。例えばコンビニアプリを想定すると、客の入店に対し店内音声と店員挨拶の2アクティビティをおこなうとする。これを実装するためにonEnterToBellハンドラとonEnterToHelloハンドラを個別に用意し、両方をEnterイベントに登録する。1つのハンドラは特定のイベントとアクティビティに特化している。

ドメインベース

特定のドメインにおける全てのイベントを扱うハンドラ。全てのイベントを引数として受け付け、そのドメインが利用しないイベントを受け付けた場合は単純にスルーし、必要に応じて処理をおこなう。例えばコンビニアプリを想定すると、店内放送ハンドラは入店イベントと会計イベントを受け付けるが、入店イベントのみに対して音声を流す処理をおこなう。1つのハンドラは特定ドメインの更新に特化しており機能的に凝集している(関心の分離)。

Flux

Fluxアーキテクチャは1つのeventがドメインごとに分けられた複数のハンドラで処理されるパターンである。Fluxの用語では action:reducer=1:Nと言える。

少女は太陽になる

ファンとアイドルの在り方の1つ.

成長を見守る物語

太陽に憧れる少女を支え、共に走る。
やがて少女は手が届かぬほど大きく、大きく成長してゆく。
かつて少女だった、燦然と輝く太陽を仰ぎ、私は微笑む。

どういうこと

そういうこと。
成長が別れとイコールな、切ないけど綺麗なコンテンツの在り方.

副作用

別れが成功の代償なので、これを理解しないでファンをやってると心が死ぬので注意。
売り出し側もこれは理解しとかないと刺されて死ぬ。

他の在り方

  • 俺たちゃ腐れ縁 (つかず離れず、目標なんてない)