オブジェクト指向の「コンポジション(Composition)」とは、あるクラスが他のクラスのインスタンスを内部に保持することで複雑な機能を実現する設計方法のことです。「継承(Inheritance)」と並ぶ、オブジェクト指向設計における重要な考え方の一つです。

なぜコンポジションを使うのか?

オブジェクト指向では、「継承」と「コンポジション」という 2 つの方法でクラス間の関係性を表現できます。
  • 継承(Inheritance) は、「is-a」の関係(〜は〜である)を表現するのに向いています。 例:「犬は動物である(Dog is an Animal)」
  • コンポジション(Composition) は、「has-a」の関係(〜は〜を持っている)を表現するのに向いています。 例:「車はエンジンを持っている(Car has an Engine)」
コンポジションを使うと、クラス間の結合度(依存関係)が低くなり、設計が柔軟になります。これは、プログラムの拡張性や保守性を高めるために重要です。

Ruby によるコンポジションの具体例

具体的な例として、Ruby で「車(Car)」クラスと「エンジン(Engine)」クラスの関係をコンポジションで表現してみましょう。

1. エンジンクラス(Engine)を作成する

# エンジンクラス
class Engine
  def start
    puts 'エンジンが始動しました!'
  end

  def stop
    puts 'エンジンが停止しました!'
  end
end

2. 車クラス(Car)を作成し、Engine のインスタンスを持つ

# 車クラス(Car)はエンジンクラス(Engine)のインスタンスを内部に持つ
class Car
  def initialize
    # Engineクラスのインスタンスを生成して内部に保持する
    @engine = Engine.new
  end

  def start_car
    puts '車を動かします。'
    @engine.start
  end

  def stop_car
    puts '車を止めます。'
    @engine.stop
  end
end

3. インスタンスを生成して実行する

my_car = Car.new
my_car.start_car
# 出力:
# 車を動かします。
# エンジンが始動しました!

my_car.stop_car
# 出力:
# 車を止めます。
# エンジンが停止しました!

なぜ継承ではなくコンポジションを使うのか?

「車」と「エンジン」の関係を継承で表現するのは不自然です。車はエンジンではない(Car is not an Engine)からです。 一方、コンポジションを使えば、「車はエンジンを持っている(Car has an Engine)」という自然な関係性を綺麗に表現できます。

アンチパターン:継承の誤用

以下は継承を誤って使用した例です:
# 悪い例:継承の誤用
class Engine
  def start
    puts 'エンジンが始動しました!'
  end

  def stop
    puts 'エンジンが停止しました!'
  end
end

# 車はエンジンではないのに、エンジンを継承している
class Car < Engine
  def drive
    puts '車を運転します'
    start  # 継承したメソッド
  end
end

# この設計の問題点:
# 1. 意味的に不自然(車はエンジンではない)
# 2. エンジンを変更できない(電気エンジンに交換不可)
# 3. 複数のコンポーネントを持てない(タイヤやブレーキを追加できない)
この例では、Car が Engine を継承していますが、これは「車はエンジンである」という誤った関係を表現しています。 さらに、コンポジションの大きな利点は「部品の交換が簡単」なことです。例えば、ガソリンエンジンから電気エンジンに変更したくなった場合、以下のように柔軟に対応できます。

依存性注入(Dependency Injection)

上記の例では、エンジンを外部から注入する「依存性注入」というパターンを使用しています。これにより、Car クラスは特定のエンジンの実装に依存せず、任意のエンジンを使用できるようになります。
# 電気エンジンのクラス
class ElectricEngine
  def start
    puts '電気エンジンが静かに始動しました!'
  end

  def stop
    puts '電気エンジンが停止しました!'
  end
end

# エンジンを交換できるようにする
class Car
  def initialize(engine)
    @engine = engine
  end

  def start_car
    puts '車を動かします。'
    @engine.start
  end

  def stop_car
    puts '車を止めます。'
    @engine.stop
  end
end

# 通常エンジンの車
normal_car = Car.new(Engine.new)
normal_car.start_car
# 車を動かします。
# エンジンが始動しました!

# 電気エンジンの車
electric_car = Car.new(ElectricEngine.new)
electric_car.start_car
# 車を動かします。
# 電気エンジンが静かに始動しました!

より複雑なコンポジションの例

実際のアプリケーションでは、クラスは複数のコンポーネントを持つことが一般的です。車の例をさらに拡張してみましょう。
# タイヤクラス
class Tire
  attr_reader :size, :type

  def initialize(size, type)
    @size = size
    @type = type
  end

  def info
    "#{@size}インチ #{@type}タイヤ"
  end
end

# ブレーキクラス
class Brake
  attr_reader :type

  def initialize(type)
    @type = type
  end

  def apply
    puts "#{@type}ブレーキをかけました!"
  end
end

# GPSナビゲーションクラス
class Navigation
  def calculate_route(destination)
    puts "#{destination}への最適ルートを計算中..."
    sleep(1)
    puts "ルートが見つかりました!"
  end
end

# 拡張された車クラス
class AdvancedCar
  def initialize(engine, tire_size: 17, tire_type: 'ラジアル', brake_type: 'ディスク')
    @engine = engine
    @tires = Array.new(4) { Tire.new(tire_size, tire_type) }
    @brake = Brake.new(brake_type)
    @navigation = Navigation.new
  end

  def start
    puts "車の準備を開始します..."
    puts "タイヤ: #{@tires.first.info} x 4本"
    puts "ブレーキ: #{@brake.type}"
    @engine.start
  end

  def stop
    @brake.apply
    @engine.stop
  end

  def navigate_to(destination)
    @navigation.calculate_route(destination)
  end
end

# 使用例
my_car = AdvancedCar.new(
  ElectricEngine.new,
  tire_size: 18,
  tire_type: 'スポーツ',
  brake_type: 'カーボン'
)

my_car.start
my_car.navigate_to("東京駅")
my_car.stop
このように、コンポジションを使うことで、各部品(エンジン、タイヤ、ブレーキ、ナビゲーション)を独立して管理でき、必要に応じて組み合わせを変更できます。

コンポジションのメリットまとめ

  • 柔軟性の向上:クラスの振る舞いを組み合わせたり変更したりしやすい。
  • 保守性の向上:各クラスが独立していて、変更が他のクラスに影響しにくい。
  • 低い結合度(Low Coupling):クラス同士が疎結合となり、コードの再利用性が高まる。

練習問題

問題 1: 基本的なコンポジション

以下の要件を満たす Computer クラスと関連するコンポーネントクラスを実装してください。 要件:
  • Computer は CPU、Memory、Storage を持つ
  • 各コンポーネントは info メソッドを持ち、自身の仕様を返す
  • Computer クラスは show_specs メソッドで全コンポーネントの情報を表示する

問題 2: 柔軟な設計

音楽プレーヤーアプリケーションを設計してください。 要件:
  • MusicPlayer クラスは、AudioDecoder と Speaker を持つ
  • AudioDecoder は MP3Decoder と FLACDecoder の 2 種類がある
  • Speaker は StandardSpeaker と HighQualitySpeaker の 2 種類がある
  • 依存性注入を使って、任意の組み合わせで MusicPlayer を作成できるようにする

解答例


まとめ

オブジェクト指向のコンポジションとは、「has-a」の関係を表現する設計手法であり、Ruby においてもシンプルかつ柔軟なコードを実現するための非常に効果的な方法です。 Ruby のような動的な言語では、特にコンポジションを活用することで、拡張性やメンテナンス性が高いコードを書くことができます。