前回のスパゲッティコードは、流れが絡み合ったコードでした。では、その流れを完璧に整理すればよいコードになるのでしょうか。
前回の記事 コードスメル #1から続く内容です。
必ずしもそうではありません。
層をきれいに分け、各層は下の層だけを呼び出すという規則を設け、インターフェースで境界を引いても、触りたくないコードになります。
文字列フィールド1つを画面に表示するだけで7ファイルを修正するコード。これをラザニアコード(Lasagna Code)と呼びます(C2原文)。
層がきれいに積み重なったコード
名前のとおりです。ラザニアはパスタとソースを何層にも重ねて作ります。
断面は整っていて層もはっきりしています。問題は、その層を1つだけ取り出せないことです。
典型的な例を見てみましょう。サーバーが返すユーザー情報にnicknameフィールドが追加され、画面に表示するだけの作業です。
1. UserDTO — サーバー応答の構造体にフィールドを追加
2. UserMapper — DTOドメインモデルへ変換するコードに1行追加
3. User — ドメインモデルにプロパティを追加
4. UserRepository — プロトコルのシグネチャが変わればここも修正
5. FetchUserUseCase — 通過するだけなのに型が合わず修正
6. UserViewModel — 画面用モデルへ再度変換
7. ProfileView — ようやく表示
DTO(Data Transfer Object、データ転送オブジェクト)から画面に届くまで、同じ文字列が3つの異なる型に格納されます。
7ファイルのうち、実際に判断している場所はいくつでしょうか。たいてい1〜2か所です。
残りは値をそのまま横へ渡すだけのコードです。
このような層をパススルー層(pass-through layer)と呼びます。
アーキテクチャパターンの文献では、リクエストが何も処理されず層だけを通過する状態をシンクホールアンチパターンとも呼びます。ラザニアコードの核心的な症状です。
なぜこうなるのか
悪意があってこうする人はいません。むしろ正反対です。
層が増える理由の多くは、よい助言を文脈なしに適用したためです。
**「レイヤーを分離せよ」を規則として受け入れた場合です。**クリーンアーキテクチャの図を見て、円の数だけフォルダを作ります。
その図は層をいくつ作るべきかを定めたものではありません。依存性は内側にだけ向けるべきだという原則を示したものです(Presentation-Domain-Data Layering)。
ところが、それをフォルダ構成に移すと意味が変わります。
**「実装ではなく抽象に依存せよ」を何にでも適用した場合です。**実装が1つしかないプロトコルが大量に生まれます。
UserRepositoryプロトコルとUserRepositoryImpl実装が1つずつ。プロトコルの役割は、コードジャンプをもう1回増やすことだけです。
テストで偽の実装を差し込む目的なら価値がありますが、その計画がないなら層が1つ増えただけです。
**「後で変更できるように」と先回りした場合です。**データベースが変わるかもしれないから抽象化し、サーバーが変わるかもしれないからもう1枚包みます。
YAGNI(You Aren’t Gonna Need It)が警告しているのは、まさにここです。
**チームが大きくなり、層に分けた場合です。**コンウェイの法則が示すように、組織のコミュニケーション構造はそのままアーキテクチャに刻まれます。
チームが4つなら層も4つになりやすく、その境界は技術的な必要性より人の境界に従います。
コストはどこから生じるのか
層が多いと何が悪いのか、具体的に見ていきましょう。
**変更コストは層の数に比例します。**フィールド1つで7ファイル。これ以上に恐ろしいのは、開発者がそれを避けようとすることです。
規則どおりに層を通す代わりに、ViewModelからAPIを直接呼ぶ近道が生まれます。
規則を守るのが面倒になると、規則を迂回するコードが生まれます。やがて層は残り、流れはスパゲッティになります。ラザニアがスパゲッティを生むのです。
**読むコストが大きくなります。**値がどこから来たのか知るには、層をたどる必要があります。
各層は短く明快でも、7回ジャンプした後には最初の質問を忘れます。
**デバッグが遅くなります。**スタックトレースが長くなり、どの層で値が壊れたか探すには各層にブレークポイントを置く必要があります。
**ビルドが遅くなります。**層をモジュールに分けている場合は特にそうです。下の層を直すたびに上の層がすべて再ビルドされます。
層が必要な場合と不要な場合
だからといって層をなくせという意味ではありません。判断基準が必要です。
次の1つの質問が、たいていうまく機能します。
「この層は何かを決定しているのか、それとも渡しているだけなのか。」
決定するとは、変換、検証、分岐、合成のいずれかを行うことです。サーバーの日付文字列をDateに変換するマッパーは決定を行います。
キャッシュがあればキャッシュを、なければネットワークを使うリポジトリも決定を行います。一方、パラメーターをそのまま受け取り、そのまま渡すUseCaseは何も決定しません。
もう1つの基準は変更理由です。サーバー応答形式の変更と画面表示ルールの変更は、異なる理由によるものです。
だからDTOと画面モデルを分ける価値があります。逆に、ドメインモデルと画面モデルが常に一緒に変わるなら、分けておく理由はありません。
表にまとめると、次のようになります。
| 状況 | 層を置くべきか |
|---|---|
| 外部応答形式と内部モデルが別々に変化する | はい |
| 実装を実際に差し替える計画がある | はい |
| テストで偽の実装が必要 | はい |
| 規模が大きく、チーム境界をコードに刻む必要がある | 条件付きではい |
| 実装が1つだけで、今後も1つである | いいえ |
| 値を受け取り、そのまま渡すだけ | いいえ |
| 図にその層があるから作った | いいえ |
層を取り除く方法
すでに積み上がったラザニアを扱う手順は、おおむね次のとおりです。
**まずパススルー層を探します。**メソッド本体が1行で、その1行が別オブジェクトの同名メソッドを呼んでいるなら候補です。
プロジェクト全体でこのパターンを検索すれば、すぐに一覧ができます。
**実装数を数えます。**各プロトコルに実装がいくつあるか確認します。1つだけでテストでも使っていないなら、プロトコルを削除して具体型を直接使います。
このとき、性能を根拠にしないほうがよいでしょう。any UserRepository同じ存在型で呼び出すと、witness tableを通るのは事実です。
ただし、プロトコルをジェネリック制約として使えば特殊化され、そのコストは消えます。逆に、finalでないクラスは、具体型で呼んでもデフォルトでは動的ディスパッチです。
さらに、ネットワークやデータベースをまたぐリポジトリ呼び出しでは、この差は実測に現れません。
プロトコルを削除する理由は性能ではなく、ジャンプ回数です。
**型をまとめます。**フィールドが同じで常に一緒に変わるDTOとドメインモデルなら、1つにします。
Codableをドメインモデルに直接付けることを汚染と見る考え方もあります。ただ、その汚染がいつ問題になるのかを先に考えるべきです。
**1つの層に畳み込みます。**層を削除するのが不安なら、層は残してファイルを統合できます。
プロトコルと実装を同じファイルに置くだけでも、ジャンプ回数は減ります。
まとめ
- ラザニアコードとは、層が多すぎて小さな変更でも複数ファイルを修正しなければならないコードです。
- 多くの場合、悪いコードを書いたからではなく、よい助言を文脈なしに適用したことで生まれます。
- 核心的な症状はパススルー層です。何も決定せず、値を横に渡すだけの層です。
- 判断基準は2つです。この層は決定するか、そして他の層とは異なる理由で変わるか。
- 規則を守るのが面倒になると迂回路が生まれます。層が多すぎると、やがてスパゲッティも一緒に育ちます。
次回は、層ではなく断片が問題になる場合です。クラスはどれも小さく、きちんとカプセル化されているのに、全体の流れを誰も説明できないコード、ラビオリコードを見ていきます。
出典と確認基準
- Lasagna Code — C2 Wiki · 著者原文 · 確認 2026-08-17 · 根拠:ラザニアコードの比喩と過剰な層の問題
- Presentation-Domain-Data Layering — Martin Fowler · 著者原文 · 確認 2026-08-17 · 根拠:層分離の目的と変更境界

![[コードスメル #2] ラザニアコード:1つのフィールド追加で7ファイルを直す理由のカバー画像](/assets/images/posts/c81880b5-e5ed-45e7-8f84-04d1a59bc481/lasagna-code-layered-architecture.jpg)