remedaのpipeを使ってデータフローをリファクタリングした話
2025/01/21
About
rkasu.icon
Infixer(Ryo Katsuse)
アウトプット
最初に🙏🙏
https://gyazo.com/9ee96ab076f97cf7b987ed12fc4227a5
今朝に登壇を決めて作ったので内容が荒いかもです。
お品書き
散らばったコードたち
データフローの設計
まとめ
散らばったコードたち😇
プロジェクトの内容
※かなり内容ぼかしています
社内プロジェクトのKPIや、売上などをグラフで可視化したダッシュボード管理画面
APIはたった1つ(クソデカJSONをフロントエンドでいい感じに整形する)
ディレクトリがぐちゃぐちゃでどこに何が入っているのかわからない
コンポーネントも関数も、基本コピペで作られていて重複している処理がたくさんある
関数名から何をしている処理なのかわからない
テストがない!!!(関数に依存も多くテストしにくい状況)
何故こうなっているのか(聴いてみた)
3ヶ月でとりあえず作らないといけなかったのでガンガン作ってしまった
フロントエンドにそこまで明るくない方が作っていた(最初ドメインロジックがClassで書かれていた) 関数型ではなく命令形で書かれていた
当然テストを書いている暇がないし、同じような処理だったとしてもコンポーネントの中で重複ロジックが生成され続けた
新たな要件
今までは特に条件分岐がなく素直に1度だけデータを整形してグラフに渡せばよかった
しかし新しいドメイン知識の追加により、条件分岐が発生してフォーマット処理が増える!
要件が追加された実装で出てきたPRをレビューして。。
重複したロジックが2倍に増えたww(コピペ)
コードジャンプで定義を追えるけど今どこにいて次どこに行くのか予測できない
今までのメンタルモデルだと関数の重複が増えて肥大化してやばいことになる・・・ https://gyazo.com/20c917e253461d3346084e0cdb12aaa1
データフローの設計
こういう流れだったのを
(複雑なので無視していいです)
code:gantt.mmd
graph TD
B --> C{Format Type}
subgraph "Common Utilities"
end
subgraph "Core Processing"
end
D --> J
D --> K
D --> L
D --> M
E --> J
E --> K
E --> L
E --> M
J --> F
J --> G
J --> H
J --> I
K --> F
K --> G
K --> H
K --> I
L --> F
L --> G
L --> H
L --> I
M --> F
M --> G
M --> H
M --> I
style J fill:#ff9999
style K fill:#ff9999
style L fill:#ff9999
style M fill:#ff9999
J -.->|Duplicate| K
K -.->|Duplicate| L
L -.->|Duplicate| M
こう変える
code:gantt.mmd
graph TD
D --> E
コードの見通しを良くしたい!
元々のPR段階での処理はAPIのデータを条件分岐した先で重複した関数をコピペしていた
これらは共通化できるはず
実現したいこととしては以下のような流れだった
APIのデータをグラフで扱えるような構造に整形してあげる その過程にそれぞれ必要な情報に整形して計算する処理になる
つまり以下がデータフロー
1. タイプチェック
2. フォーマット
2. 計算
4. 微調整して出てきた出力をそのままグラフに渡す
モデル層みたいなものを作る
データを一方通行にすれば処理の流れがわかりやすく
各データフローごとに関数を正しく抽象化する
各処理でI/Oの型を決めて出力されたものを次のステップに渡す
ちゃんとドキュメントとしてルールを決めて合意形成をとる remedaとは
ある入力を順番に複数の関数に通して、最終的な出力を得るヘルパー関数のこと
code:typeScript
R.pipe(data, op1, op2, op3);
R.pipe(
R.map((x) => x * 2),
(arr) => [arr0 + arr1, arr2 + arr3], 今回はこのpipeを使用した
とりあえず既存の実装を一箇所に集める
pipe適用前
code:Typescript
const transformKpiDataWithoutPipe = (input: KpiData): FinalData => {
// 1. 入力チェック
const validatedData = checkKpiData(input);
// 2. TypeA/TypeB に応じたテンプレート生成
let templateData: IntermediateData;
if (validatedData.type === 'TypeA') {
templateData = generateStandardTemplate(validatedData);
} else {
templateData = generateCustomTemplate(validatedData);
}
// 3. 共通フォーマット
const formattedData = commonFormat(templateData);
// 4. 計算
const calculatedData = calculate(formattedData);
// 5. 最終フォーマット(微調整)
const finalData = finalFormat(calculatedData);
return finalData;
};
どの出力が次の入力になるか定数を追わないといけないのでぱっと見では追いにくい。
流れが見えにくい
pipeを使用する
code:typescript
const transformKpiDataWithPipe = (input: KpiData): FinalData => {
return pipe(
input,
// 1. 入力チェック
(data) => checkKpiData(data),
// 2. TypeA / TypeB のテンプレート生成
(data) => data.type === 'TypeA'
? generateStandardTemplate(data)
: generateCustomTemplate(data),
// 3. 共通フォーマット
(data) => commonFormat(data),
// 4. 計算
(data) => calculate(data),
// 5. 最終フォーマット
(data) => finalFormat(data)
);
};
一方向に処理が流れていることが視覚的に分かりやすくなった
各関数が入力・出力の型を明確に持つため、型チェックもしやすく拡張性が高い。
テストもしやすくなった→テストを書けた🎉 🎉
一箇所にまとめる際にやったこと
整形する関数の中で計算ロジック関数が呼ばれていたので分離する
要はデータフローごとに関数を抽象化しなおす
これにより型の入出力が明確になった
何が変わったのか?
処理の流れがわかりやすくなった
テストをしっかり書けるようになった(品質担保)
しかしフロントエンドでやっている処理は余り変わっていない
もっとよくするためには
API設計をちゃんとやった方がいい
流石に1つのAPIを無理くりフォーマットしたらそりゃパフォーマンスも落ちますよね。。。
スキーマの定義も扱いやすいようにすればそもそも今回のデータフローも不要になるかもしれない
今後やっていきたい
まとめ
抽象化を間違えない
大きなドメインロジックはデータフローを考えてからルールを決める
フロントエンドだけで無理矢理やらない(異論は認める)
ご清聴ありがとうございました🙌🙌