今回やること
この記事では、長い関数を小さな関数へ分けます。
関数分割の目的は、行数を減らすことではなく、読む人が処理の流れを理解しやすくすることです。
前提条件
- TypeScriptの関数が読める
- if文、throw、async/awaitが分かる
- リファクタリング前後で動作を変えない意識がある
Step 1: 長い関数を見る
async function createOrder(input: OrderInput) {
if (input.items.length === 0) {
throw new Error("items required");
}
let total = 0;
for (const item of input.items) {
total += item.price * item.quantity;
}
if (total <= 0) {
throw new Error("invalid total");
}
const order = {
userId: input.userId,
items: input.items,
total,
};
await orderRepository.save(order);
return order;
}
入力チェック、合計計算、注文作成、保存が1つに入っています。
Step 2: 処理のまとまりを見つける
コメントを付けるなら、次のように分かれます。
- 入力チェック
- 合計金額の計算
- 注文オブジェクトの作成
- 保存
コメントで説明したくなるまとまりは、関数化の候補です。
Step 3: 合計計算を切り出す
function calculateOrderTotal(items: OrderItem[]) {
return items.reduce((total, item) => {
return total + item.price * item.quantity;
}, 0);
}
合計計算はDBに依存しないため、単体でテストしやすくなります。
Step 4: 入力チェックを切り出す
function validateOrderInput(input: OrderInput) {
if (input.items.length === 0) {
throw new Error("items required");
}
}
入力チェックのルール変更は、この関数を見ればよくなります。
Step 5: 元の関数を流れだけにする
async function createOrder(input: OrderInput) {
validateOrderInput(input);
const total = calculateOrderTotal(input.items);
if (total <= 0) {
throw new Error("invalid total");
}
const order = {
userId: input.userId,
items: input.items,
total,
};
await orderRepository.save(order);
return order;
}
外側の関数は、処理の流れを読むための場所になりました。
よくあるエラー
| 状況 | 原因 | 対応 |
|---|---|---|
| 関数を細かくしすぎる | 1行ごとに切り出している | 意味のある単位だけ分ける |
| 引数が多すぎる | 切り出す範囲が不自然 | データのまとまりを見直す |
| 動作が変わった | リファクタ中に仕様変更した | 先にテストや出力で固定する |
| 名前が付けられない | 責務が混ざっている | もう一段処理を整理する |
まとめ
長い関数は、入力チェック、計算、外部I/Oなど、変更理由が違う単位で分けると読みやすくなります。行数だけでなく、責務とテストしやすさを見て判断します。