
この記事は hacomono Advent Calendar 2025 の20日目の記事です
みゅーとんです. 今月 3 本目. 頑張りました.
はじめに
業務コードを書いていると,「長い関数をどう分割すればいいか」という相談を受けることがあります.
話を聞いていると, 多くの場合これは新規で書かれるコードの時点ですでに長大で複雑になっているケースが多くありました.
この記事では、新しく業務ロジックを書く場面を想定して,
- どんな要件のコードなのか
- それを素直に書くとどうなりがちか
- そのとき自分はどう考えて実装しているか
という流れで整理してみます.
サンプルコードの実装要件
今回扱うサンプルの前提です. 以下の要件をフロントエンド側で TypeScript で記載することを想定します.
- カート情報をもとに請求を確定する
- 処理の中では次のことを行う
- カート内容の検証
- 利用実績の取得
- 金額の計算
- 割引の適用
- 請求情報の保存
- 請求確定の通知
- 非同期処理を含む
- 新規実装で、まだ既存コードはない
業務コードとしては, よくありそうな要件だと思います.
何も考えずに書く
この要件を満たす, 愚直に一つの関数で実現するコードを ChatGPT に出力させました. わざと様々な可読性や品質を落とすように出力させています.
async function confirmBilling(cart: Cart): Promise<BillingResult> { let amount = 0 let billingResult: BillingResult | null = null if (!cart || !cart.items || cart.items.length === 0) { console.warn('cart is empty', cart) throw new Error('empty cart') } const usageResponse = await fetch(`/usage/${cart.contractId}`) const usage = await usageResponse.json() for (let i = 0; i < usage.length; i++) { amount += usage[i].price } if (cart.isMember) { amount = amount * 0.9 } if (amount < 0) { amount = 0 } try { const billingResponse = await fetch({ method: 'post', path: `/billing/${cart.id}`, body: { amount: amount, member: cart.isMember, itemCount: cart.items.length, }, }) billingResult = await billingResponse.json() } catch (e) { console.error('billing failed', e) } if (billingResult) { await fetch({ method: 'post', path: `/notify/${billingResult.id}`, body: { message: billingResult.result === 'success' ? '決済が完了しました' : '決済に失敗しました', retry: billingResult.result !== 'success', }, }) } return billingResult as BillingResult }
新規実装として見ると, 特におかしなことはしていません.
ただ, この関数には次のような特徴があります.
- 処理がすべて1か所に集まっている
- コメントがないと何をしているか, 俯瞰して分かりにくい
- 後から処理が増えたときに, 自然に長くなっていく
結果として, 「気づいたら長くなっている関数」 になりがちです。
なぜ分割されないまま書くのか
社内で話を聞いてみると, 分割されにくい理由はだいたい次の2つに集約されました.
- 分割するのは再利用のため, という意識に引っ張られる
- まず一気に書いて, あとから分割するつもりでいる
その結果, 分割して書くこと自体に “追加の工数” が発生してしまっている. もしくは, そういう認識が持たれているように感じました。
これも, 新規実装ではとても自然な流れだと思います。
最初から分割する手法
動詞で処理を説明する
自分の場合, いきなり処理を書き始めることはあまりありません.
まずやるのは, この関数が何をしているかを, 文章で並べること です.
今回の例だと, 次のようになります.
async function confirmBilling(cart: Cart): Promise<BillingResult> { // カートを検証する // 利用実績を集計する // 金額を計算する // 請求を保存する // 通知する }
この時点では, まだコードは書きません。
コメントをそのまま動詞の関数にする
次に考えるのは単純で, このコメントをそのまま関数名にする ことです.
書き換えるとこうなります.
async function confirmBilling(cart: Cart): Promise<BillingResult> { validateCart(cart) const usage = await collectUsage(cart) const amount = calculateAmount(cart, usage) const billingResult = await saveBilling(cart, amount) await notifyBilling(billingResult) return billingResult }
この時点では, collectUsage や calculateAmount の中身を深く考えていません. 先に決めているのは, どんな操作があるか, どんな順番で進むかだけです.
実装は, その後に回します.
この段階ではまだ 未実装の関数が使われている状況によるエラーだらけ ではあるけれど,
- 少ない行数で関数が書き終わっている
- if や細かい判断が見えなくなる
- 上から読むだけで処理の流れが分かる
という状態になります.
関数を実装する / 同じことを繰り返す
未実装の関数を詰めていく作業になります. ここでも, 同じように内部の処理を複数の動詞で説明可能であれば, 同じやり方で分割してしまってよいでしょう.
例えば collectUsage で行いたい処理が仮に複雑であった場合, 再帰的に同じ対応をすれば良いだけです.
個人的には,
- ざっくり考えて行数が多くなりそうなとき
- 説明しづらい時
- 1回コンビニに行って返ってきてから同じ関数を読んだ時, 30秒かからず何やってるか理解できる
を目安に分割すべきかどうかを判断しています.
型もあとから決める
この例では collectUsage の返り値の型が必要になります, 実際にはこの型がまだ存在しない状態で分解を進めている状況になります.
この段階で必要なのは, “利用実績(Usage型)” という概念がある という合意だけです.
型の詳細は, その型を引数とする関数が処理として何を要求しているか?を元に決めれば良いです. つまり, 後から詰められます.
分解の目的は, 型を完成させることではなく, 責務の境界を決めること だと考えています.
補足:細かい処理の置き場所について
ここまでの作業を実践すると, 一つのファイルに分割された細かい関数が大量に並ぶことが想像できるかと思います.
自分はこの構造あまり好んでおらず, _internal のようなフォルダ名で区切ってさらにファイルを分けるような対応をしています. ここでは confirmBilling からしか呼ばない関数を別ファイルに切り出し, _internal フォルダ配下に配置しています.
├ confirmBilling/
├ confirmBilling.ts
└ _internals/
├ cart.ts
├ usage.ts
├ calculate.ts
_internals のようなフォルダを切っておくと,
- 外部から使う関数
- 内部実装としての関数
を分けて考えやすくなります.
また, この構造は結果として,
- ファイル単体が長くなりすぎない
- 細かいビジネスロジック単位でテストを書きやすい
といったメリットを感じることが多いです.
まとめ
新規に業務ロジックを書くとき, 自分が意識しているのは次のような点です.
- まず処理を文章で並べる
- 文章を動詞の関数にする
- 中身は後で考える
- 実装時も, 同じことを繰り返す
- 必要に応じて, 置き場所も分ける
- コンビニに行って小休憩をとり, 脳内メモリをクリーンにする
最初から細かく設計しようとせず, まず分ける.
説明できなくなったらまた分ける.
このやり方が, 長い関数を実装せずに済むことが多いと感じています.
他の型のコーディングの参考になれば幸いです.