関数分割の単位はどう見極めるか?長い関数のレビュー練習

関数の肥大化は、Goに限らずあらゆる言語で発生する構造的課題です。特にGoは関数がシンプルかつ宣言的に書けるため、“なんとなく短く”ではなく、“設計的に意味のある単位”で分割されているかがレビュー観点として重要です。

この記事では、レビューアーとして関数分割の是非を判断するための構造的観点、読みやすさだけでない分割の意義、そして改善の方向性を提示します。


良い実装例 - 責務に応じた関数分割

func HandleRequest(req *http.Request) error {
    input, err := parseInput(req)
    if err != nil {
        return err
    }

    if !checkAuth(input.UserToken) {
        return errors.New("unauthorized")
    }

    result, err := execute(input.Command)
    if err != nil {
        return err
    }

    return respond(result)
}

func parseInput(req *http.Request) (*Input, error) {
    // 入力解析処理
}

func checkAuth(token string) bool {
    // 認可チェック処理
}

func execute(cmd string) (string, error) {
    // ビジネスロジック実行処理
}

func respond(output string) error {
    // 応答返却処理
}
技術解説

このコードは4つの主要な責務(解析・認可・実行・応答)を分離し、それぞれを独立した関数として抽出しています。関数単体でのテストや再利用がしやすく、エラー処理も粒度を保ったまま局所的に行えています。
レビューにおいては、関数ごとの設計意図が明確かどうか、結合度が過度に高くなっていないか、再利用性が担保されているかを確認します。


良くない実装例 - 関数が肥大化して責務が不明瞭に

func HandleRequest(req *http.Request) error {
    // 入力解析
    input := &Input{}
    body, err := io.ReadAll(req.Body)
    if err != nil {
        return err
    }
    if err := json.Unmarshal(body, input); err != nil {
        return err
    }

    // 認可
    if input.UserToken != "admin-token" {
        return errors.New("unauthorized")
    }

    // 実行
    var result string
    switch input.Command {
    case "echo":
        result = input.Payload
    case "reverse":
        result = reverse(input.Payload)
    default:
        return errors.New("unknown command")
    }

    // 応答
    _, err = fmt.Fprintf(req.Context().Value("writer").(io.Writer), result)
    return err
}
@Reviewer
この関数は4つの異なる責務を1つに詰め込んでいます。`parseInput`, `checkAuth`, `execute`, `respond` といった処理を分割し、各機能の単体テストや修正時の影響範囲を明確化しましょう。

チェックポイント:レビューで確認すべき関数分割の観点

観点 チェックするポイント
複数責務の混在 処理の性質が明確に異なるコードが同居していないか?
コメントによるセクション分け // parse, // execute のように関数内でブロック化していないか?
ネストの深さ ifswitchが3段以上にネストしていないか?
ローカル変数の多さ 7個以上の変数が宣言されていないか?用途は統一されているか?
副作用の混在 ネットワーク/ファイル操作などの副作用が複数責務にまたがっていないか?
単体テストの難しさ 一部だけをテストできる設計になっているか?セットアップの負荷が高くないか?
抽象化の有無 関数が構造や制約を提供しているか?単なる処理転送になっていないか?

追加観点:設計意図を伝えるための命名と粒度調整

関数名は処理内容を説明するのではなく「その関数が担う責任」を表現するのが望ましいです。
たとえば checkAndExecute() よりも、checkAuth()executeCommand() に分割する方が責務が明確になります。


関数分割は責務と構造のリファクタリング

関数分割の目的は単なる「短くすること」ではありません。
設計の視点から見ると、関数とは構造の最小単位であり、責務の境界線です。レビュー時には、次のような問いかけを意識すると構造的判断がしやすくなります。

  • この関数は“何をする”のではなく“何を担う”のか?
  • この中に設計上の“複数の視点”が混ざっていないか?
  • テスト観点から見たときに“分けるべき”単位になっているか?

複雑な関数を見かけたら、その中に潜む構造上の境界線を見つけ出し、適切な関数分割を提案する。
それが、レビューアーとしての重要な役割のひとつです。