🌆

関数の多重下請けをやめよう。単一責任の原則と関数の"責任"について

2025/01/26に公開

「多重下請け構造は悪い」、これは世間的にだいぶ浸透してきた考えだと思います。しかし、プログラマは 多重下請けのコードを気づかぬうちに書いてしまうことが多々あります。 なんなら皆さんもついウッカリやってしまっているでしょう。

当然ながらコードベースでも多重下請けは良くありません。今回の記事では、多重下請けコードとは何か、その問題点、回避方法を解説します。

多重下請け構造になってるコードとは?

多重下請け構造になってるコードとは、タスクをたらい回しにしているコードです。

たとえばECサイトで注文するシーンを考えてみます。サーバーの実装はこんな感じです。

def 注文API():
    注文処理(price)
    集計DBにログを送る()

def 注文処理(price):
    決済する(price)
    履歴に保存する()
  
def 決済する(price):
    if キャンペーン期間中だったら:
        支払う(price * 0.8)
    else:
        支払う(price)
    注文完了メールを送る()

一見するとなにも悪くなさそうです。似たコードを書いたことある人も多いでしょう。しかし、このコードは 多重下請けという問題を抱えています。 たとえば決済処理タスクは何度もたらい回しになっています(注文API注文処理決済する支払う)。


任されたタスクをそのまま下の関数に投げており、多重下請けに陥っている

多重下請けには様々な問題があります。

  • どの関数が何を担当しているか分からない
  • 各関数の命名が難しい
  • リファクタがしづらい

問題1: どの関数が何を担当しているか分からない

例えば、コードから「注文完了メールはどのタイミングで送信されるか」を読み解いてみてください。メール送信タスクがたらい回しされているせいで、 コード全体を読まないと場所の検討さえつかないでしょう。 最初から注文APIが注文完了メールを送る()関数にタスクを任せるべきでした。

見当もつかないままコード全部を読んでいく作業はとてもキツイです。いちいち全部読み直さないといけない状況でのバグ調査なんて、とても最悪な体験です。DXが悪い。

余談: そもそも関数の「責任」とは

ここまででフワッと「責任」という単語を使ってましたが、そもそもこれはどういう意味でしょうか?

僕は 「任されたタスクをバグなく実行すること」が関数の果たすべき責任 だと思います。

任されたタスク(責任)の数が増えていくほどに、その関数は複雑でバグりやすく、見通しが悪くなります。なので関数の責任は1つに絞るべきです。

問題2: 各関数の命名が難しい

多重下請けでは担当するタスクが多いため、関数の適切な命名が難しいです。 たとえば上の例で注文処理関数には複数のタスク(決済、メール送信、履歴保存)が任されています。その結果、注文処理という曖昧な名前をつけるしかなくなっています。

名前が曖昧だとコードを読み解くのが困難になります。たとえば決済タスクの責任の所在はコードの詳細を全部読まねばわかりません。コードの詳細を全部読む必要があるため、やはり多重下請け構造はカスです。

問題3: リファクタがしづらい

多重下請けはコードの可読性以外に、リファクタ時にも影響します。なぜなら複数のタスクを抱えた関数をいじることになり、リファクタしたときに他機能への影響も考慮しなければいけなくなるためです。単一責任の原則(関数を変更する理由はひとつだけであるべきである)にも思いっきり違反しています。

また、多重下請け構造ではテストが書きづらいです。テストの書きやすさはリファクタのしやすさに強く影響するため、なおさらリファクタがしづらいです。

どうすれば良いか: 関数に任せるタスクを1つに絞る

すべての問題の原因は、関数に複数のタスクを任せていることです。 言い換えると、単一責任の原則に違反していることです。

たとえば、決済する関数は支払いと完了メールの送信という、2つの機能を担当していて、単一責任の原則に違反しています。注文処理関数に至っては履歴保存機能もあわせて、3つの機能を実現する責任を負っています。

こうならないようにするには、タスクを分割してやればよいのです。実際にやってみましょう。まずサーバーが行っているタスクを整理すると、以下のようになります。

  • キャンペーン期間中だったら全商品20%オフする
  • 注文情報を履歴に保存する
  • 注文完了メールを送信する
  • 集計用のログを送信する

これをもとにAPIを修正してみます。

def 良い注文API():
    if キャンペーン期間中だったら:
        支払う(price * 0.8)
    else:
        支払う(price)
    履歴に保存する()
    注文完了メールを送る()
    集計DBにログを送る()


タスクのたらい回しがなくなって委譲関係がフラットになった

APIがやっていることは変わりませんが、 ずいぶんシンプルになり見通しが良くなりました。 修正後のコードは各関数が担当している役割も明白です。また、API全体がどういうことをするのかも良く分かります。

注意: フラットにしようという話ではない

注意してほしいのは、 フラットにすれば良いという話ではないということ です。まとまった単位で分割するのであれば、下層でさらに分割を行ってもよいです。

たとえば上の 注文API の話も、省略しただけで実際には 履歴API顧客API などがあるはずです。これらのAPIのなかでもタスク分割が行われているでしょう。


サーバーのなかにAPIがいくつかあり、そのAPIそれぞれで更にタスクが分割されている

渡されたタスクを分割せずに下に任せることが悪いのです。

分割時にまとまりを意識する

タスク分割のベストプラクティスについては、すでに整理されています。凝集度がそれです。 凝集度とはまとめ方の良し悪しについての指標です。 まとめ方が良いと凝集度が高いと呼ばれます。関数が凝集度が高くなるよう意識すると、良いコードになりやすいです。

特に凝集度が高いものは以下の3つです。

  • 同じデータに対する操作がまとめられた関数 (通信的凝集)
  • 一部の処理の出力が別の処理の入力となるような関数 (逐次的凝集)
  • 単一のタスクを実現する関数 (機能的凝集)

サンプルコードでいうと、履歴に保存する関数や注文完了メールを送る関数は凝集度が高く(機能的凝集)、まとまりの良い関数です。逆に、注文処理関数は複数のタスクが混ざっていて凝集度が低いです。

一般に、 多重下請けは凝集度が低い状態です。 意味のあるまとまりになるよう関数を作って凝集度を高め、多重下請けをやめましょう。

(凝集度の詳細については他の記事に解説を任せます。たくさん良い記事があるのでそちらを参考にしてください。)

まとめ: 高凝集になるように関数を作って、多重下請け構造をやめよう

ここまで見てきたように、 関数に複数の別々のタスクを任せた状態はよくない状態です。 コードを読む際の負荷が高くなりがちで、リファクタも難しくなります。

それを避けるためにも、タスクを整理して分割し、凝集度が高くなるように関数を作りましょう。

多重下請けは意識してないと簡単に陥ります(特にあとから修正する際)。一朝一夕では改善は難しいので、日々のなかでタスクの分割と委譲について意識していくことが大切です。

Discussion