コンテンツにスキップ

カレーを作るな、カレールーを入れた煮込みを作れ:抽象化と命名の入門

概要

Qiita 人気記事「カレーを作るな、カレールーを入れた煮込みを作れ」をベースにした、超新人エンジニア向けの抽象化と命名の入門。「カレーを作る」という言葉が抽象化された概念であるのと同様に、関数やメソッドの名前も「何をするか」ではなく「何であるか(抽象レベル)」で命名すべき、という考え方。

詳細

カレーのメタファー

「カレーを作る」と言うとき、人はそれが「玉ねぎを炒め、肉を炒め、水を入れて煮込み、カレールーを入れてさらに煮込む」という一連の操作であると分かった上で、それをまとめて「カレーを作る」と呼んでいる。

逆に「カレールーを入れた煮込み料理を作る」と言うと、実装の詳細が名前に漏れ出している。この名前では「カレーを作る」という概念レベルの理解がない。

プログラミングにおける命名も同じ。実装の詳細を名前に入れると、それは「抽象化できていない証拠」になる。

悪い命名:実装が見える名前

// 悪い例: 「MySQLのusersテーブルにユーザーを追加する」という実装が見えている
func AddUserToMySQLUsersTable(user User) error {
    _, err := db.Exec("INSERT INTO users ...", user)
    return err
}

// 呼び出し側
AddUserToMySQLUsersTable(newUser)
// → 呼び出し側が「MySQLのusersテーブル」という実装を知ってしまう

この命名の問題点: - DB を PostgreSQL に変えたら関数名が嘘になる - 呼び出し側が「実装の詳細」に依存する - 「ユーザーを登録する」という概念が見えない

良い命名:抽象化された名前

// 良い例: 「ユーザーを登録する」という概念レベルの名前
func RegisterUser(user User) error {
    _, err := db.Exec("INSERT INTO users ...", user)
    return err
}

// 呼び出し側
RegisterUser(newUser)
// → 呼び出し側は「ユーザーが登録される」という事実だけを知る

RegisterUser は DB の種類、テーブル名、SQL の書き方を知らなくていい。「ユーザーを登録する」という抽象概念だけを表現している。

抽象化とは「実装の詳細を隠す」こと

抽象化とは、複数の具体的な操作をまとめて「一つの概念」として扱えるようにすること。

// 抽象化前: 実装の詳細が散らばっている
rows, err := db.Query("SELECT * FROM users WHERE email = ?", email)
// ... rows を処理 ...
// ... エラーハンドリング ...
// → 呼び出し側が「DBのクエリ方法」を知っている

// 抽象化後: 概念として扱える
user, err := userRepo.FindByEmail(email)
// → 呼び出し側は「メールアドレスでユーザーを見つける」という概念だけを知る

呼び出し側が実装の詳細を知らなくていい状態にすることが、抽象化の本質。

命名は「理解の深さ」を反映する

良い命名ができないとき、それは「まだ問題を十分に理解していない」サインであることが多い。

例:

// 命名に詰まる例
func ProcessData(data []byte) error
// → 「処理する」「データ」という雑な言葉は理解が浅い証拠

// 理解が深まった後の命名
func ValidateAndStoreInvoice(invoice Invoice) error
// → 「何を」「どうする」が明確

「どう名前を付けるか分からない」「とりあえず Process にしておく」という状況は、その処理が何をしているのかを自分が分かっていないサイン。

抽象レベルを揃える

一つの関数・クラスの中では、同じ抽象レベルの操作を扱うべき。

// 抽象レベルがバラバラ(悪い例)
func CreateOrder(order Order) error {
    // 抽象レベル高: ビジネスロジック
    if !order.IsValid() {
        return ErrInvalidOrder
    }
    // 抽象レベル低: SQL の詳細
    _, err := db.Exec("INSERT INTO orders (id, user_id, ...) VALUES (?, ?, ...)", ...)
    return err
}

// 抽象レベルが揃っている(良い例)
func CreateOrder(order Order) error {
    if !order.IsValid() {
        return ErrInvalidOrder
    }
    return orderRepo.Save(order) // ← 同じ抽象レベル
}

いつこの考え方を使うか

コードレビュー時: - 「この関数名、実装の詳細が漏れていませんか?」 - 「呼び出し側がこの名前から何を知ることになりますか?」

API 設計時: - エンドポイント名が実装に依存していないか確認する - POST /mysql-insert-userPOST /users のような抽象化

リファクタリング時: - 長い処理を関数に切り出すとき、「この処理は一言で何と言えるか」を先に考える

なぜ重要か / いつ使うか

  • 新人エンジニアが「関数名の付け方が分からない」と感じているとき
  • レビューで「これ何をしているか分かりにくい」というフィードバックが多いとき
  • コードが「実装の変更のたびに呼び出し側も変わる」という問題が起きているとき
  • DDD(ドメイン駆動設計)やクリーンアーキテクチャを学ぶ前の下地として
  • 「抽象化とは何か」を人に説明しなければならないとき