エンジアップ エンジアップ

もう迷わない。ITエンジニアのための総合情報サイト

関数設計の基本:1関数1責務の原則と実践
投稿
X LINE B! f

関数設計の基本:1関数1責務の原則と実践

関数設計の基本:1関数1責務の原則と実践

プログラミングを始めてしばらく経つと、「コードは動いているのに、なぜか誰も触りたがらない」という状況に遭遇することがあります。そのコードをよく見ると、1つの関数が数百行に渡っていて、ユーザー入力の検証からデータベースへの保存、メール送信まで、あらゆる処理を一手に担っていたりします。筆者自身も駆け出しの頃、「処理をまとめた方が効率的だ」という誤った直感のもと、そういったコードを量産していた記憶があります。

この記事では、エンジニアとして長く働き続けるための基礎体力となる「1関数1責務(Single Responsibility)」の考え方を、具体的なコードと照らし合わせながら丁寧に解説します。

なぜ長い関数は問題なのか

読みにくさは「認知負荷」の問題

人間の脳が一度に処理できる情報量には限界があります。心理学では「ワーキングメモリ」と呼ばれる概念がありますが、コードを読むときも同様に、頭の中で「今どの変数が何の値を持っているか」「どの条件分岐に今いるか」を同時に追跡しなければなりません。

100行を超えるような関数では、その追跡コストが急激に上がります。あるバグを修正しようとして関数を開いたとき、関係のない処理が視界に入り、集中力が削がれる経験はないでしょうか。これは怠慢ではなく、人間の認知特性として自然な反応です。

テストが書けない=品質が保証できない

もう一つの深刻な問題はテスタビリティです。多くの処理を抱えた関数は、テストを書こうとしたときに初めてその問題が表面化します。

例えば、以下のような関数があったとします。

def register_user(form_data):
    # バリデーション
    if not form_data.get("email"):
        raise ValueError("メールアドレスは必須です")
    if "@" not in form_data["email"]:
        raise ValueError("メールアドレスの形式が不正です")
    if len(form_data.get("password", "")) < 8:
        raise ValueError("パスワードは8文字以上必要です")

    # パスワードのハッシュ化
    import hashlib
    hashed = hashlib.sha256(form_data["password"].encode()).hexdigest()

    # データベース保存
    db = get_db_connection()
    user_id = db.execute(
        "INSERT INTO users (email, password) VALUES (?, ?)",
        (form_data["email"], hashed)
    ).lastrowid

    # メール送信
    send_welcome_email(form_data["email"], user_id)

    return user_id

このコードは一見まとまっているように見えますが、テストを書こうとすると困ります。バリデーションだけをテストしたくても、データベース接続が必要になってしまいます。メール送信を切り離してモック化しようとしても、関数の途中にハードコードされているため難しい状況です。

1関数1責務の原則とは

「1関数1責務(Single Responsibility Principle)」とは、ひとつの関数(あるいはクラス)は、ひとつのことだけに責任を持つべきだという設計原則です。SOLID原則の「S」に当たります。

重要なのは「ひとつのこと」の定義です。これは「コード量が少ない」という意味ではありません。「変更する理由がひとつである」という意味です。

先ほどの register_user 関数を例にとると、変更が必要になる理由が複数考えられます。

  • バリデーションルールが変わったとき
  • パスワードのハッシュアルゴリズムを変更したいとき
  • データベースのスキーマが変わったとき
  • ウェルカムメールの内容を変えたいとき

これらはそれぞれ異なる「変更の理由」であり、つまりこの関数は複数の責務を抱えているということになります。

リファクタリングの実践

では、実際にどう分割すればよいのでしょうか。先ほどのコードを責務ごとに分解してみます。

def validate_user_form(form_data: dict) -> None:
    if not form_data.get("email"):
        raise ValueError("メールアドレスは必須です")
    if "@" not in form_data["email"]:
        raise ValueError("メールアドレスの形式が不正です")
    if len(form_data.get("password", "")) < 8:
        raise ValueError("パスワードは8文字以上必要です")


def hash_password(plain_password: str) -> str:
    import hashlib
    return hashlib.sha256(plain_password.encode()).hexdigest()


def save_user_to_db(email: str, hashed_password: str) -> int:
    db = get_db_connection()
    return db.execute(
        "INSERT INTO users (email, password) VALUES (?, ?)",
        (email, hashed_password)
    ).lastrowid


def register_user(form_data: dict) -> int:
    validate_user_form(form_data)
    hashed = hash_password(form_data["password"])
    user_id = save_user_to_db(form_data["email"], hashed)
    send_welcome_email(form_data["email"], user_id)
    return user_id

この分割によって何が変わったか整理してみましょう。

観点分割前分割後
テストのしやすさDB接続なしにバリデーションテスト不可各関数を独立してテスト可能
変更の影響範囲一部の変更が全体に波及しうる変更は該当関数のみに限定される
読みやすさ処理の意図把握に時間がかかる関数名だけで処理内容が把握できる
再利用性特定の文脈でしか使えない各関数を他の場所でも利用可能

register_user 関数を見れば、ユーザー登録がどんな手順で行われるかが一目で分かります。詳細を知りたければ各関数を掘り下げる。これが「読みやすいコード」の本質です。

関数を分けるときの迷いどころ

どこまで細かく分けるべきか

「じゃあ1行ごとに関数にすべきか?」という疑問が湧くかもしれません。もちろんそれは過剰です。目安としては、「関数の中身を見なくても、名前だけで何をするか分かる」ことが重要です。

validate_email_format(email) という関数があれば、中を見なくてもメールアドレスのフォーマット検証をするとわかります。これで十分です。一方、process(data) という名前では何も伝わりません。名前が付けにくいと感じた場合、それは関数の責務が曖昧なサインでもあります。

関数の行数の目安

厳密なルールはないものの、多くのチームでは「1関数20〜30行以内」をひとつの目安にしています。それを超えてきたら「この処理は独立した名前を付けられないか」と自問してみましょう。

引数が多すぎる関数に注意

引数が4つ以上になってきたら、関数が多くの処理を抱えているサインです。引数を構造体やオブジェクトにまとめるか、処理を分割することを検討しましょう。

# 引数が多く、呼び出し時に何が何かわからない
def create_order(user_id, product_id, quantity, coupon_code, is_express, address):
    ...

# 関連するデータをまとめる
def create_order(user_id: int, order_details: OrderDetails) -> Order:
    ...

チームで働くときの視点

1関数1責務の原則は、個人の開発でも有効ですが、チームで働くときにその価値が真価を発揮します。

コードレビューの場面を想像してみてください。分割された関数であれば、レビュアーは「この関数がやっていること」を素早く把握し、「正しくやっているか」に集中できます。一方、何でもやる関数は読み解くだけで時間がかかり、見逃しが生じやすくなります。

また、複数人が同じファイルを触る場合も、責務が明確に分かれていれば、それぞれの変更が衝突しにくくなります。Gitのコンフリクトが減るというのは、小さいようで実は大きなメリットです。

よくある誤解:「分けると呼び出しが増えて遅くなる?」

「関数を細かく分けると、呼び出しのオーバーヘッドが増えてパフォーマンスが落ちるのでは」という疑問を持つ方もいます。

現代のプログラミング言語やコンパイラは、このような関数呼び出しを最適化する仕組みを持っています。アプリケーションレベルのコードにおいて、適切な関数分割によってパフォーマンスが問題になることは、ほぼありません。それよりも、コードが複雑になって正しく動かなくなるリスクの方がはるかに深刻です。

パフォーマンスが本当に問題になるのは、計測によって特定のボトルネックが見つかったときだけです。「読みやすさより速さを優先する」という判断は、その後でも遅くありません。

まとめ

1関数1責務の原則は、難しい概念ではありません。「この関数、何者?」という問いに一言で答えられるように設計する、それだけのことです。

最初は「分けすぎかな」と感じるくらいでも問題ありません。コードを読む自分自身や未来のチームメンバーへの思いやりとして、名前の付けられる単位で関数を作る習慣を身につけてください。その積み重ねが、1年後・3年後に「このコード、読みやすい」と言われるエンジニアへの道につながります。

小さな関数の集合体が、大きな信頼を生み出す。そのことを忘れないでほしいと思います。