🍜

ラーメンで学ぶオブジェクト設計スタイルガイド

に公開

Matthias Nobackさんによる名著『オブジェクト設計スタイルガイド』を読みました。
実装方針そのものは理解できているものの、それぞれのプラクティスの名称が実装方針と紐づけて覚えることができていません。
もう少し身近なものと紐づけることで理解を整理できないかと思い、オブジェクトの構造をどう作るかに関する概念を、ラーメンの「太さ・硬さ・油指定」に照らして整理しました。

※比喩やコード例は筆者独自の整理です。厳密な仕様・詳細は各言語や原典に従ってください。


大枠:継承系 と コンポジション系

今回整理する対象は、大きく 継承系(静的に構造を固定/配布)コンポジション系(動的に組み合わせ/後付け) に分かれます。

継承系(静的に固定/配布)

  • ねらい:手順・契約を強制して品質と一貫性を担保する
  • 代表:継承 / テンプレートメソッド / トレイト(Mixin)
  • 強み
    • 手順の骨格を固定できる(Template Method)
    • 契約・共通実装を横断配布できる(Trait/Mixin)
  • 弱み/リスク
    • 変更が親から全体に波及しやすい
    • 組み合わせが増えるとクラス爆発や依存の見えにくさが生じる
  • 使いどころ
    • 「この順序・契約は破らせない」安全性が最優先な部分
    • ライブラリやフレームワーク側の拡張ポイントに乗る場合

コンポジション系(動的に組み合わせ/後付け)

  • ねらい:部品を組み合わせて差し替えやすく、変更に強い構造を作る
  • 代表:コンポジション / デコレーター / 委譲
  • 強み
    • 部品単位で差し替え可能、テスト容易、拡張が局所化
    • 既存コードに触れず後付けで機能追加(Decorator)
    • 責務を分けて内部オブジェクトに任せられる(Delegation)
  • 弱み/リスク
    • デコレーター多層化で追跡が難しくなることがある
    • 委譲が増えすぎるとただの転送クラス化の恐れ
  • 使いどころ
    • 機能組み合わせが多様・可変(A/B差し替えが頻発)
    • 既存コードへの影響を極小化したいとき

原則:まず コンポジション を優先し、
それで表現しにくい「骨格の強制」「横断配布」が必要な箇所に限って 継承系 を選ぶ。


共通ゴール(全プラクティスで同じ出力)

ramen(miso, thick, hard, oil=more)

1. 継承(Inheritance : is-a)

定義:親クラスから性質・振る舞いを引き継ぐ関係
ラーメン比喩「太さ・硬さ・油」を固定した一品を派生クラスとして用意

  • メリット
    • 共通処理を親に集約できる
    • テンプレートメソッドのようにアルゴリズムの骨格を固定できる
  • 注意
    • 親の小さな変更でも子クラス全体に影響が及ぶ
    • クラス数が爆発しやすい
    • 親子の関心が混在しやすい

継承を使う動機が「共通化したい」だけなら、 コンポジション を検討する。

コードサンプル

from abc import ABC, abstractmethod

class Ramen(ABC):
    def desc(self): return f"ramen({self.soup()}, {self.thickness()}, {self.hardness()}, oil={self.oil()})"
    @abstractmethod
    def soup(self): ...
    def thickness(self): return "thin"
    def hardness(self):  return "normal"
    def oil(self):       return "less"

class MisoThickHardMore(Ramen):
    def soup(self):      return "miso"
    def thickness(self): return "thick"
    def hardness(self):  return "hard"
    def oil(self):       return "more"

print(MisoThickHardMore().desc())

2. テンプレートメソッド(Template Method)

定義:アルゴリズムの骨格を親で定め、差し替え部分をに任せる(継承ベース)
ラーメン比喩「調理手順は固定、味だけ子クラスで決定」=麺茹で・盛付けは共通、スープだけ差し替え

  • メリット
    • 手順の共通化と拡張性の両立(一定品質を保ちやすい)
  • 注意
    • 継承ベースのため結合が強い
    • コンポジションで代替できる場面が多い

コードサンプル

from abc import ABC, abstractmethod

class Template(ABC):
    def __init__(self, thick, hard, oil): self.thick, self.hard, self.oil = thick, hard, oil
    def desc(self): return f"ramen({self.soup()}, {self.thick}, {self.hard}, oil={self.oil})"
    @abstractmethod
    def soup(self): ...

class Miso(Template):
    def soup(self): return "miso"

print(Miso("thick", "hard", "more").desc())

3. トレイト(Trait : 行動契約+既定実装)

定義:型が満たすべき行動契約(メソッド集合)や既定実装を提供できる仕組み
ラーメン比喩「硬さ・油の指定ルール」を全メニューに一括配布する

  • メリット
    • 横断的な振る舞いを複数クラスに再利用できる
    • 「替え玉OK」など共通ルールを適用可能
  • 注意
    • 多重適用でルール衝突に注意
    • Javaのinterfaceは契約中心、Scalaのtraitはコード再利用寄りなど言語差が大きい(契約中心か再利用中心か)

コードサンプル

class OptionsMixin:
    def set(self, thick, hard, oil): self.thick, self.hard, self.o = thick, hard, oil
    def build(self, soup): return f"ramen({soup}, {self.thick}, {self.hard}, oil={self.oil})"

class MisoRamen(OptionsMixin):
    def desc(self): return self.build("miso")

r = MisoRamen(); r.set("thick", "hard", "more")
print(r.desc())

4. コンポジション(Composition : has-a/強い所有)

定義:部品を組み合わせて機能を構成する(強い所有関係を持つ)
ラーメン比喩スープ・太さ・硬さ・油を部品として組み合わせて作る

  • メリット
    • 責務分離と再利用性の向上
    • 部品単位で入れ替え可能(太麺×硬め×油多め など自在)
  • 注意
    • 全体(丼)が消えると部分(中身)も消える=寿命が一体

コードサンプル

class Ramen:
    def __init__(self, soup, thick, hard, oil): self.soup, self.thick, self.hard, self.oil = soup, thick, hard, oil
    def desc(self): return f"ramen({self.soup}, {self.thick}, {self.hard}, oil={self.oil})"

r = Ramen("miso", "thick", "hard", "more")
print(r.desc())

5. デコレーション(Decorator : wrap-and-extend/動的拡張)

定義:オブジェクトをラップして振る舞いを後付けで追加する
ラーメン比喩「普通ラーメン」に太さ・硬さ・油の指定を後からトッピングする

  • メリット
    • 元の構造を壊さず機能を段階的に追加できる
    • 組み合わせ次第で柔軟に拡張可能
  • 注意
    • ラッパ層が増えると呼び出し経路が不透明になる
    • 層が増えるとデバッグやテストが困難になる

コードサンプル

class Deco:
    def __init__(self, inner): self.inner = inner
    def desc(self): return self.inner.desc()

class Thick(Deco):
    def desc(self): return f"{self.inner.desc()}, thick"

class Hard(Deco):
    def desc(self): return f"{self.inner.desc()}, hard"

class OilMore(Deco):
    def desc(self): return f"{self.inner.desc()}, oil=more"

order = OilMore(Hard(Thick(Base())))
print(order.desc())  # ramen(miso, thick, hard, oil=more)

6. 委譲(Delegation : 振る舞いを任せる)

定義:あるオブジェクトが受け取った処理を、内部の別オブジェクトに任せる
ラーメン比喩店主が「太さの決定」を麺職人に任せる

  • メリット
    • 責務分離が明確になり再利用性が高まる
    • 継承の強い結合を避けつつ振る舞いを再利用できる
  • 注意
    • 委譲が多すぎると処理経路が冗長になる
    • 何でも委譲すると「転送クラス化」する恐れ

コードサンプル

class ThicknessChef:
    def decide(self): return "thick"

class RamenShop:
    def __init__(self, chef): self.chef = chef
    def desc(self): return f"ramen({self.chef.decide()}, hard, oil=more)"

shop = RamenShop(ThicknessChef())
print(shop.desc())  # ramen(thick, hard, oil=more)


まとめ:構造設計の判断軸

  • 継承系(静的固定)

    • 継承:分岐が増えるとクラス爆発しやすい
    • テンプレートメソッド:骨格固定+フックで品質を確保
    • トレイト/Mixin:横断の共通処理を配布(多用注意)
  • コンポジション系(動的差し替え)

    • コンポジション:最小で柔軟。まずこれを優先
    • デコレーター:既存改変なしで段積み拡張(過剰装飾注意)
    • 委譲(Delegation):処理を内部オブジェクトに任せて責務を明確化(多用すると冗長)

👉 設計原則:まずコンポジション、必要なら委譲、継承は最小限に。


おわりに

ラーメンの世界観に置き換えると、抽象的な設計の判断軸が身体感覚で掴みやすくなります。
現場ではまず コンポジション を基盤に、必要な箇所でデコレーショントレイトを選ぶ。
継承テンプレートメソッドは結合が強いため、必要最小限に留めるのが推奨されます。🍜

Discussion