みんな大好き re-ducks。
最初に知ったときは、そのタイプの切り方に「なるほどなー」と唸らされました。
現在でもre-ducksをベースとしているのですが、3年の間の紆余曲折を共有したいと思います。
対象読者
- re-ducksの実装経験がある人
- 大きめのアプリケーションを長期に運用している、したい人
- FEエンジニア
導入直後
標準のReduxディレクトリ構成に比べると、ファイルの見通しが良く、機能修正などの際も同じディレクトリ内で差分が収まるなどし、Reduxコードの扱いがかなり楽になりました。
その後の変化
ducksのカオス化
長く運用していくとサービスの成長とともにコードも肥大化し、開発者もどんどん入れ替わります。
すると導入当初の設計思想が伝わらないことも多く、その時々の案件に応じたducksが作られてしまいました。
特定のコンポーネントでしか使えない
例えば同じリストデータでも、ナビゲーション用とフォーム用では取得方法や使いやすいデータ形式が変わってきたりします。本来であれば、シンプルなstateに、それぞれに応じたoperation/selectorを用意して対応するのが理想ですが、現実ではUIごとに最適化されたducksが増産されていきました。
ただこれが絶対悪かというとそうとも言えず、逆にその方が開発が容易だと考えることもできます。
アプリケーションの状態を作っているとはいえ、やはりUIと近い位置にあるため、どうしてもそこに引きづられやすいようです。
理想と現実の差の原因
主に下記が原因と考えられます。
- 全体設計や既存コードへの理解不足
- 影響範囲の最小化などの考慮
- データ単位よりもコンポーネント単位のducksの方が考慮が少なくて済む
前半2点は大きいアプリケーションを長く運用していくとどうしても生じてしまう問題かと思います。
最後の1点は、コードの最適化という面で考えればデータ単位が良いと思われますが、限られた開発コストで機能追加や変更を行っていくと、短期的に安全な方を選択してしまうことが多いようです。
人の入れ替わりや、業務委託さんとの協業などを考えると、理想を追い続けて理想からかけ離れた状態が作られてしまうより、コストや開発容易性を考慮したバランスの良い落とし所を決めて、そこよりは質を落とさないというのを意識した方が良いと感じました。
対策
その上で私たちのチームでは次のような対策を取り入れました。
色々な対策は考えられるのですが、コードベースで向き合う方法として導入した2例を紹介します。
1.ducksは結合テストを書く
よくあるReduxのテストとして、redux-mock-store
を用いたテストがよく紹介されています。
私たちも当初は利用しており、re-ducks導入後も継続していました。
しかし、この場合、それぞれのファイル(actions/operations/reducers/selectors)でのテストが必要になり、またducksとしての提供機能の保証としてかなり弱いテストになってしまっていました。
そこで実施したのが結合テストです。
公式読めよという感じですね。
特徴的なのは、middleware(主にthunk)を考慮したoperationsもテストできるように、本来と同じようにcreateStoreを実施しているところです。そうなると実行コストや実装コストが上がってしまうのですが、概念はわかりやすく説明が容易なので、サンプルコードを書いてこんな感じでお願いしたいです、という感じで回しています。
メリット
- ducksとしての機能をしっかりと保証することができる
- 安心してリファクタリングや機能拡張ができるようになる
デメリット
- テストの実装、実行コストは上がる(ただ説明や依頼はしやすい)
2.action/operatorはジェネレーターを用意する
前述のリストデータの例のように、理想的には使いまわしたいが現実的に使い回さないことになるケースがありました。やはり同じコードを使うというのは様々な考慮や調査が必要になったりします。
それであれば、ロジックは使いまわすがコードはそれぞれ生成すればいいじゃない(別物にする)、という考えです。
具体的な例がこちらです。
import { createAction } from '@reduxjs/toolkit';
import * as api from '~/api/hogeList';
export const createHogeOperations = (name: string) => {
const actions = {
start : createAction(`${name}/HOGE_START`),
success : createAction<HogeResult>(`${name}/HOGE_SUCCESS`),
error : createAction<Error>(`${name}/HOGE_Error`),
};
// operations
const get = (request: HogeRequest) => async dispatch => {
try {
dispatch(actions.start());
const result = await api.get(request);
dispatch(actions.success(result));
} catch (e) {
dispatch(actions.error(e));
}
};
return {
actions,
get,
};
};
メリット
- hogeList
というデータを扱うための取得オペレーションや関連処理を使いまわすことができる
- type名を動的に生成しているため、他のモジュールを気にすることなく利用することができる
- ジェネレーターはアプリケーションに詳しい人が設計開発し、そうでない人にコンポーネント+ducksの開発を任せることができる
デメリット
- 慣れが必要
対策後のducksイメージ
まとめ
- re-ducksは素敵だけどducksがカオス化した
- 現状を見て現実的な対策2案を実施、紹介