そのController、全部自分でやろうとしてない?😤

そのController、全部自分でやろうとしてない?😤

これ絶対あるじゃん??これ絶対あるじゃん??

  • Controllerのファイルを開いたら、SQLが直接書いてある
  • UserControllerの中に、バリデーション・メール送信・パスワードハッシュが全部ある
  • 「メールの件名変えて」って言われたのに、なぜかControllerを800行読む羽目になった
  • 新人に「ビジネスロジックどこに書けばいいですか?」って聞かれて「……とりあえずControllerに」って答えた

これがFat Controller(ファットコントローラー)、以降「なんでもフロントマン」って呼ぶね、が原因なんだよね。


てかシンプルに言うとねてかシンプルに言うとね

なんでもフロントマンってのは、「リクエストを受け取る・ビジネスを判断する・データを保存する」を、1つの層にぜんぶ押し込んだ設計のことなんだよね。

それの何がまずいの?ってなる気持ち、わかる!動いてるうちはマジで気づかないし、最初はむしろ「一箇所にあって便利じゃん」ってなる。でも「1行変えたつもり」が予想外の場所で爆発するの、全部ここが原因なんだよね。変更の影響がどこで止まるか、誰にも読めない状態が続いてくんだよ。


ちょっと待って、先に全然関係ない話していい?🏠ちょっと待って、先に全然関係ない話していい?🏠

やまだクリニックっていう、開院3年目の小さな内科があったんだけどさ。院長の田中先生は、ぶっちゃけスタッフが全然足りてないその職場で、受付・診察・会計・調剤を1人でぜんぶこなしてたの。

「田中先生、次の患者さんどうぞ〜」

診察室で患者を診ながら、田中先生は心の中で 「いや受付どっから声かけてんの、今診察中だけど」 と思いつつ「はーい」と返した。診察が終わった瞬間、今度は会計窓口に走る。薬の説明もある。次の患者も待ってる。

それでも3年間、なんとかなっていた。

問題が起きたのは、10月のことだった。

「田中先生!保険点数の計算ルールが変わったんですけど、請求書の計算式はどうしたらいいですか?」

「え、ちょっと待って。計算式…あれ、どこだっけ。受付のメモ?診察記録の後ろ?」

田中先生は自分の頭の中を必死に検索した。会計のルールは、診察記録の横に手書きでメモしてあった。でもその紙、先週の診察で使った後どこに置いたっけ。

その日の午後、請求書が3件、金額違いで患者に渡った。 ヤバくない?

翌日、患者から電話が来たんだよね。

「請求額がおかしいんですが」

「申し訳ありません、確認します」

確認するために田中先生は診察記録を探した。そこに会計メモが書いてあったんだけど、田中先生の手書きのメモの読み方は田中先生しかわからないじゃん。誰も確認できない。3日後、クリニックの口コミに「会計でミスが多い」と書かれた。患者が1人、別のクリニックに移ったんだって。

「計算ルールを1個変えただけなのに、なんでこんなことに…」

これが本番で起きてるやつが、なんでもフロントマンってこと。


じゃあコードで見てみよっか👀じゃあコードで見てみよっか👀

class UserController:
    def create_user(self, request):
        # バリデーション
        if not request.email or "@" not in request.email:
            return {"error": "メールアドレスが不正です"}
        if len(request.password) < 8:
            return {"error": "パスワードは8文字以上にしてください"}

        # パスワードのハッシュ化
        import hashlib
        password_hash = hashlib.sha256(request.password.encode()).hexdigest()

        # DB直接操作
        db = Database()
        existing = db.query(
            f"SELECT id FROM users WHERE email = '{request.email}'"
        )
        if existing:
            return {"error": "このメールアドレスはすでに登録されています"}

        user_id = db.execute(
            f"INSERT INTO users (email, password_hash) "
            f"VALUES ('{request.email}', '{password_hash}')"
        )

        # メール送信
        smtp = SmtpClient()
        smtp.send(
            to=request.email,
            subject="登録完了のご案内",
            body=f"ユーザーID {user_id} で登録が完了しました。"
        )

        # ログ記録
        Logger().write(f"[INFO] user created: id={user_id}, email={request.email}")

        return {"user_id": user_id, "status": "created"}

このControllerがやってること、数えてみて。

  1. バリデーション(入力の検証)
  2. パスワードのハッシュ化(セキュリティ処理)
  3. DB重複チェック(データ操作)
  4. DBへのINSERT(データ操作)
  5. メール送信(外部サービス連携)
  6. ログ記録(監視・デバッグ)

6つある。

ここで「登録完了メールの件名を変えて」って言われたとする。メール送信のコード、このControllerの中にある。Controllerを開く必要がある。Controllerを読む必要がある。Controllerをテストし直す必要がある。「件名1行変えた」だけのはずが、このファイル全体を触るリスクを背負うんだよね。

わたしも昔これやったから言えるんだけど、「とりあえず動かしたい」って気持ちでなんでもフロントマンにしてくと、あるとき突然「このファイル怖くて変えられない」ってなるんだよね。深夜のSlack通知で気づいたときには、どの変更が原因かわからなくなってる💔


やばいのまだあんだけど、バグに気づけない問題😭やばいのまだあんだけど、バグに気づけない問題😭

なんでもフロントマンが怖いのって、「壊れやすい」だけじゃないんだよね。**「壊れてても気づけない」**んだよ。

やまだクリニックの田中先生で考えてみて。保険点数の計算が正しいかチェックしたい。でも田中先生は受付・診察・会計・調剤を全部やってる。「会計だけ」を切り出してテストできない。受付の状態・診察の記録・調剤の流れ、全部が揃ってないと会計の確認ができないんだよね。

コードも同じことが起きてる。

def test_create_user():
    # このテスト、実は全処理を同時に検証してる
    # バリデーション × DBアクセス × メール送信 × ログ記録
    # → 組み合わせが爆発する

    # パターン1: 正常な登録
    # パターン2: メールアドレス不正 × DBが落ちてる
    # パターン3: パスワード短い × SMTPサーバー落ちてる
    # パターン4: 重複ユーザー × ログが書けない
    # ...考えるだけで組み合わせが無限に出てくる

    request = FakeRequest(email="test@example.com", password="password123")
    db = FakeDatabase()   # DBのモックが必要
    smtp = FakeSmtp()     # SMTPのモックが必要
    logger = FakeLogger() # ログのモックが必要

    controller = UserController(db=db, smtp=smtp, logger=logger)
    result = controller.create_user(request)

    assert result["status"] == "created"
    # これ、何をテストできてる?ぜんぶ?それとも何も?

バリデーションのロジックを変えたとき、DBの動作まで一緒に再テストしなきゃいけない。メール文面を変えたとき、ユーザー作成のロジックまで一緒に検証することになる。テストケースの数が「全機能の組み合わせ」になってく。テストが重くなる。テストが書きにくくなる。テストが書かれなくなる。バグが潜る。

なんでもフロントマンは壊れやすいだけじゃなく、バグを隠す設計でもある。


分けてた会社の話もするね〜✨分けてた会社の話もするね〜✨

同じ時期に開院した「かわむらクリニック」の話もするね。こっちはスタッフが同じ人数でも、役割がはっきり決まってたの。

  • 受付スタッフ:患者の来院記録・保険証確認
  • 田中先生(診察):診察・診断・処方箋の作成
  • 会計担当:保険点数の計算・請求書の発行
  • 薬剤師:処方箋をもとに調剤

「保険点数の計算ルールが変わりました」って通達が来た日。

「了解です」と会計担当が言って、自分の担当範囲だけ修正した。田中先生の診察は変わらない。受付の記録フローも変わらない。薬剤師も何もしなくていい。翌日、請求書は正確に発行された。

患者のクレームゼロ。田中先生は定時で帰れた。

コードで書くとこう。

# Controller(Presentation層の入口): HTTPリクエストを受け取ってServiceに渡すだけ
# ビジネスの判断は一切しない
class UserController:
    def __init__(self, user_service: UserService):
        self.user_service = user_service

    def create_user(self, request):
        try:
            user = self.user_service.create(
                email=request.email,
                password=request.password,
            )
            return {"user_id": user.id, "status": "created"}
        except ValidationError as e:
            return {"error": str(e)}


# Service(ユースケースの調整役): PresとDomainをつなぐ薄い層
# ビジネスルールの核はUserエンティティ(Domainオブジェクト)が持つ
class UserService:
    def __init__(self, user_repo: UserRepository, mailer: Mailer):
        self.user_repo = user_repo
        self.mailer = mailer

    def create(self, email: str, password: str) -> User:
        # 重複チェックはRepositoryに任せる
        if self.user_repo.exists_by_email(email):
            raise ValidationError("このメールアドレスはすでに登録されています")

        # Userエンティティを生成(バリデーションとハッシュ化はエンティティ内に持たせる)
        user = User.create(email=email, password=password)

        # 保存はRepositoryに任せる
        saved_user = self.user_repo.save(user)

        # メール送信はMailerに任せる
        self.mailer.send_welcome(to=email, user_id=saved_user.id)

        return saved_user


# Userエンティティ(Domain層): ビジネスルールの核はここに持たせる
class User:
    def __init__(self, email: str, password_hash: str):
        self.email = email
        self.password_hash = password_hash
        self.id = None

    @classmethod
    def create(cls, email: str, password: str) -> "User":
        # バリデーション(ビジネスルール)はDomainオブジェクトが知っている
        if not email or "@" not in email:
            raise ValidationError("メールアドレスが不正です")
        if len(password) < 8:
            raise ValidationError("パスワードは8文字以上にしてください")
        return cls(email=email, password_hash=hash_password(password))


# Repository(Data Source層): データの読み書きだけ。ビジネスロジックを持たない
class UserRepository:
    def __init__(self, db: Database):
        self.db = db

    def exists_by_email(self, email: str) -> bool:
        result = self.db.query(
            "SELECT COUNT(*) FROM users WHERE email = %s", (email,)
        )
        return result[0] > 0

    def save(self, user: User) -> User:
        user_id = self.db.execute(
            "INSERT INTO users (email, password_hash) VALUES (%s, %s)",
            (user.email, user.password_hash),
        )
        user.id = user_id
        return user

「メールの件名変えて」→ Mailerクラスだけ開けばいい。Controllerは触らない。Serviceも触らない。

「バリデーションルール変えて」→ Userエンティティだけ変える。DBもメールも1行も変わらない。

「DBのテーブル構造変えたい」→ UserRepositoryだけ変える。ビジネスロジックは1行も変わらない。

変更が1箇所で止まる。 これが層を分けた設計の力なんだよね。


ちゃんとした名前もあるから一応言うね📚ちゃんとした名前もあるから一応言うね📚

**レイヤードアーキテクチャ(Layered Architecture)**ってのは、アプリの処理を「役割ごとの層」に分ける設計のやり方。

ルールはシンプルで、上の層は下の層に頼れるけど、下の層は上の層を知らない。一方通行っつーこと。

ここで一個だけ正確にしておきたいんだけど、Fowlerの元ネタの本(P of EAA, 2002)が決めた3層の名前はこうなんだよね。

Fowlerが決めた層名対応するコードの例
Presentation(UI層)Controller, View, Router
Domain(ビジネスロジック層)UserエンティティなどのDomainオブジェクト
Data Source(データアクセス層)Repository, DAO

「Controller / Service / Repository」ってよく言われるやつは、この3層をRailsやSpring界隈の慣習に落とし込んだ呼び方なんだよね。ControllerはPresentation層の入口の一部で、Presentation層全体ではない。

もう1個大事なことがあって、Serviceは「薄く」するのがFowlerの考えなんだよね。Fowlerは「Serviceはできるだけ薄くしたい」派なの。ビジネスの一番大事な判断はServiceじゃなくてDomainオブジェクト(エンティティ)が持つべきっていう立場。Serviceは「段取りする係」で、判断する係じゃない。

「ビジネスの判断はServiceに全部書く」ってやり方はRailsの界隈のクセで、Fowlerが最初に言った意味とはちょっとずれてるんだよね。

依存の方向はPresentation → Domain → Data Sourceの一方向。この一方通行のルールさえ守れば、「隣の層にだけ頼るカッチリ版」でも「下の層ならどれでもOKなゆるめ版」でも、どっちでもいけるんだよね。

  • まとめたのはMartin Fowler。本『Patterns of Enterprise Application Architecture』(2002年, Addison-Wesley)で整理された。
  • Eric EvansもDDD(ドメイン駆動設計)でこの考え方を発展させ、Domain層をどう守るかを掘り下げている。
  • 日本語では「階層型アーキテクチャ」とも呼ばれる。

難しそうに聞こえるけど、要は「この処理、誰の仕事?」を問い続けるだけなんだよね。


これだけ覚えて帰って👌これだけ覚えて帰って👌

合言葉はこれ。nfy(なんでもフロントマンやめろ)

コードを書くとき・レビューするとき、この問いを使って。

「この処理が変わる理由、何種類ある?」

さっきのなんでもフロントマンで考えてみて。

  • メール文面が変わる → マーケが決める
  • バリデーションルールが変わる → 企画が決める
  • DBのテーブル構造が変わる → インフラが決める
  • パスワードのハッシュアルゴリズムが変わる → セキュリティが決める

変わる理由が4種類 = 変更を引き起こす「出来事」が4種類 = 田中先生が4人必要ってこと。

1つのクラスに「書き直すきっかけ」が何個もあるなら、それは分けるサイン。

Controllerはリクエストを受けてServiceに渡すだけ。
Serviceは段取りするだけ。
Domainオブジェクトはビジネスの判断をするだけ。
Repositoryはデータの読み書きだけ。

それぞれの「書き直すきっかけ」が1種類になるまで分けてくんだよね。


まとめるとこういうことねまとめるとこういうことね

比較項目なんでもフロントマン(層の越境)Controller / Service / Repository の分離
変更の影響範囲1か所の変更がController全体に波及する変更が担当層だけで止まる
テストのしやすさ全機能の組み合わせをまとめてテストする必要がある各層を独立してテストできる
バグの発見しやすさどの処理の問題か判別しにくい層が分かれているので原因が絞り込みやすい
新メンバーの理解速度どこに何が書いてあるか全部読まないとわからない役割ごとに読む範囲が決まっている
変更への恐怖「何が壊れるかわからない」がずっと続く変更前に影響範囲が把握できる

レイヤードアーキテクチャは「難しい設計」じゃなく、「変更を怖くない状態にする設計」なんだよね。なんでもフロントマンにするの、今日で終わりにしてこ。


沼りたい人はこっちも読んで📖沼りたい人はこっちも読んで📖

権威ある書籍

  • Martin Fowler『Patterns of Enterprise Application Architecture』(2002年, Addison-Wesley)
    レイヤードアーキテクチャを含むエンタープライズ設計パターンを網羅した原典。Controller/Service/Repositoryの背景にある3層(Presentation/Domain/Data Source)の概念がここから来ている。
  • Eric Evans『Domain-Driven Design』(2003年, Addison-Wesley)
    Domain層をどう設計するかを深く掘り下げている。「ビジネスロジックの核はDomainオブジェクトが持つ」の考え方の出所。
  • Robert C. Martin『Clean Architecture』(2017年, Prentice Hall)
    依存関係の方向性と層の役割について、SRPと絡めながら実践的に解説している。

一次資料

無料で読める入門記事


Dev-Here「ギャルでも分かる設計の勘ドコロ!」シリーズ 第9回