テストとコード品質

[コードの臭い #3] ラビオリコード、誰も流れを把握できないコード

関数はすべて5行で責務も1つずつなのに、ボタンを押すと何が起きるか誰も答えられないコードがラビオリコードです。過度な分割の理由とジャンプ回数に表れるコスト、流れを取り戻す方法をまとめました。

読了 6 分
[コードの臭い #3] ラビオリコード、誰も流れを把握できないコードのカバー画像

コードレビューで、こんな状況があります。ファイルを開くと、関数がすべて5行以内です。

前回の記事 コードの臭い #2 の続きです。

名前も適切で、各クラスの責務も1つだけです。指摘するところがありません。

ところが、「このボタンを押すと何が起きますか」と聞かれても、誰もすぐには答えられません。

このようなコードをラビオリコード(Ravioli Code)と呼びます(C2原文)。

悪口だけではありませんでした

面白いことに、この言葉はいつも悪い意味で使われるわけではありません。ラビオリは、小さな一片ずつが具を包み込むパスタです。

適切にカプセル化された小さなオブジェクト、つまりオブジェクト指向が目指していた姿そのものです。実際、スパゲッティコードの対極としてラビオリを挙げる用例もあります。

問題は数です。

一つひとつは完璧でも、皿には200個並び、どの順序でどうつながるかはどこにも書かれていません。

具体的には、次のような状態です。

final class CheckoutCoordinator {
    func start() { validator.validate(cart) }
}

final class CartValidator {
    func validate(_ cart: Cart) { stockChecker.check(cart.items) }
}

final class StockChecker {
    func check(_ items: [Item]) { priceCalculator.calculate(items) }
}

final class PriceCalculator {
    func calculate(_ items: [Item]) { paymentPreparer.prepare(items) }
}
// ... このようなクラスがさらに16個あります

各クラスに非の打ち所はありません。名前は正確で、仕事も1つです。

しかし、決済フロー全体を把握しているファイルは一つもありません。

流れはクラス間の呼び出し関係としてしか存在せず、コードを読むだけではなくデバッガーを付けて初めて分かります。

なぜ分割しすぎるのか

「関数は短いほどよい」という規則として受け入れた場合。 『Clean Code』は、関数は2、3行、長くても4行にすべきだとまで述べています。

この助言が本来狙っていたのは、1つの関数が1つの抽象化レベルだけを扱うことです。しかし行数という数字に置き換わると、目的が失われます。

結果は5行の関数が30個、そのうち28個はたった1か所からしか呼ばれません。

名前が内容を要約できていない場合。 handleUserAction、processData、updateState のような名前では、何をするのか分かりません。

このような名前の関数に分けると、読者は結局本文を開く必要があり、分けた分だけ確認箇所が増えます。

よい抽出は本文を見なくても済むようにするものですが、逆効果です。

抽象化レベルが混在している場合。 1つの関数に「注文を検証する」というポリシーレベルの文と、「インデックスを1増やす」という詳細レベルの文が混在しています。すると視線が絶えず上下します。

この状態で無計画に分割すると、レベルがばらばらの断片が生まれます。

一度しか使わないコードを再利用に備えて切り出した場合。 ラザニアコードを作る心理と同じです。方向が縦から横に変わっただけです。

チェーン状につながるラビオリクラス構造とオーケストレーター関数構造の比較図
流れを書いておく場所を1つ設けるだけで大部分は解決します

コストはジャンプ回数に表れる

ラビオリコードのコストは一文で要約できます。1つの質問に答えるため、ファイルを何回開くのか。

コードを読むとき、人は短期記憶に流れを保持します。この記憶は3、4個を超えるとぼやけます。

ファイルを6回も行き来すると、最初に何を探していたかが曖昧になり、また最初に戻ります。

これが繰り返されると、コードを読むより実行して確かめるほうが速くなります。

副作用も生じます。

  • 新機能を追加するとき既存の断片を見つけられず、似たものをもう1つ作ります。重複が静かに増えていきます。
  • バグの修正箇所を見つけられず、フローの末端、つまり画面側に一時的な処理を追加します。
  • フロー全体がコードに書かれていないため、文書か人の頭の中にしか残りません。その人がチームを去れば、一緒に消えます。

流れを取り戻す方法

**流れを書く場所を1つ作ります。**最も効果の大きい対策です。

全体の順序を一目で示す関数を1つ置き、その関数には順序だけを語らせます。

func checkout(_ cart: Cart) async throws -> Receipt {
    try validate(cart)
    try await reserveStock(cart.items)
    let amount = calculateTotal(cart)
    let payment = try await charge(amount)
    return try await confirm(cart, payment)
}

詳細は引き続きそれぞれの場所にあります。変わったのは、流れが1画面に書かれている点です。

このような関数をオーケストレーターと呼ぶこともあります。ラビオリコードに欠けているのは、まさにこの場所です。

**1つの層には1つのレベルだけ置きます。**上の関数の5行はすべて同じ高さの文です。ここにitems.count > 0のような詳細条件が入ると、レベルが崩れます。

関数を1つ読んだとき、文が同じ目線の高さにあるか確認する習慣は、分割基準より役立ちます。

**抽出するかどうかは名前で決めます。**基準は行数ではなく、次の問いです。「この断片に付ける名前は、本文より多くを語るか」。

if user.age >= 19をisAdult(user)として切り出せば意図が明らかになるので有益です。array.append(item)をaddItemとして切り出しても何も得られません。

**近いものは近くに置きます。**一緒に変わるコードは同じファイル、同じフォルダーに置きます。

ファイル1つにつき型1つという慣例を守るため、いつも一緒に開く型が分散しているなら、慣例より近接性を優先します。

**サービス単位にも同じ基準を適用します。**マイクロサービスを細かく分けすぎた状態をナノサービスと呼びます。

1つのリクエストを処理するために12個のサービスを経由する構造は、ラビオリコードがネットワークの向こう側まで拡張された形です。この場合、ジャンプのコストに遅延と障害点まで加わります。

スパゲッティ、ラザニア、ラビオリという3つのコードの臭いの構造を並べて比較した画像
3つとも断片単位では満点です

ラザニアとラビオリ、そしてスパゲッティ

3つを並べると、次のように整理できます。

形態 断片の状態 問題
スパゲッティ 大きく絡み合っている 実行フローを予測できない
ラザニア 縦に何層も重なる 変更1つで複数の層を貫く必要がある
ラビオリ 小さく整っている 断片間の流れがどこにもない

3つとも全体を把握するコストが大きいだけで、方法が違うだけです。

コード品質を断片一つひとつの美しさだけで測ると、ラザニアとラビオリは見抜けません。どちらも断片単位では満点だからです。

まとめると

  • ラビオリコードとは、小さく整った断片が多すぎて、全体の流れを誰も説明できないコードです。
  • カプセル化されたコードを肯定的に指す用例もありました。問題は数です。
  • 主な原因は、行数のルール、曖昧な名前、混在した抽象化レベルです。
  • 対策の核心は、流れを書いておく場所です。
  • 抽出基準は長さではなく名前です。名前が本文より多くを語る場合だけ切り出します。

次回は正反対の極端です。断片が多すぎるのではなく、1つしかないケースです。

クラス1つでアプリ全体を把握するゴッドオブジェクトを見ていきます。

出典と確認基準

  • Ravioli Code — C2 Wiki · 原著者 · 確認 2026-08-17 · 根拠:小さくカプセル化されたオブジェクトが過度に分断されるラビオリコードの比喩

あわせて読みたい