🦁

継承とその弊害と対策

2024/09/06に公開

まず継承とは

親クラスのプロパティやメソッドを子クラスで引き継ぐこと。子クラスで同様の機能が使用できることで、同様のコードを再度記述する必要がなくなり、コードの再利用性と拡張性が向上する。

親クラスのプロパティやメソッドを全て引き継いで、子クラスでさらに拡張するような形

# 親クラス
class Vehicle
  def start
    "Starting the vehicle"
  end
end

# 子クラス
class Car < Vehicle
  def drive
    "Driving the car"
  end
end

class Bicycle < Vehicle
  def pedal
    "Pedaling the bicycle"
  end
end

car = Car.new
puts car.start  # => "Starting the vehicle"
puts car.drive  # => "Driving the car"

bicycle = Bicycle.new
puts bicycle.start  # => "Starting the vehicle"
puts bicycle.pedal  # => "Pedaling the bicycle"

Vehicleという親クラスをもとにして、CarBicycleという子クラスを作成している。Vehicleクラスは、startメソッドを定義していて、これは子クラスに継承することで共通の動作として働くようになっている。またCarBicycleクラスは、この親クラスを継承しつつ、それぞれのクラスに特有の動作を定義している状態。

継承は「is-a」関係を前提としており、今回の例ではCarBicycleVehicleであるという関係にあるため適切である。しかし、開発を進めていく中でこの関係が崩れると以下のような弊害を生むことがある。

継承による弊害

  1. 密結合: 親クラスと子クラスの間で強い依存関係が生じること。継承では、子クラスが親クラスのメソッドやプロパティを直接使用するため、親クラスに変更が加えられると、それが子クラス全体に影響を及ぼす可能性が高くなる。このような関係は、システム全体の柔軟性を損なう原因となり、コードの変更が非常に難しくなる場合がある。
  2. 保守性の低下: 親クラスに変更を加えると、その変更がすべての子クラスに影響を及ぼしてしまう。これにより、親クラスを変更するたびに子クラスの動作を確認し、必要に応じて修正する必要が生じる。特に大規模なシステムでは、親クラスの変更が多数の子クラスに影響を与えるため、修正作業が膨大になる可能性高くなる。子クラスは、親クラスの構造を常に気にしなければならなくなってしまう。
  3. 不要な機能の露出: 子クラスは親クラスのすべてのメソッドとプロパティを継承するため、そのクラスにとって不要な機能も利用可能に。これにより、意図しないメソッドが誤って使用されるリスクが増え、バグが発生する可能性が高まってしまう。また、コードの可読性が低下し、開発者がシステムの全体像を理解しにくくなるため、保守作業が複雑化していく。
  4. 拡張性の制限: 新しい機能や要件を追加する際、親クラスに変更を加える必要があり、その影響が子クラスにも及ぶため、拡張作業が複雑化。また、親クラスの設計が将来的な変更や拡張を考慮していない場合、システム全体の設計を見直す必要が生じることがある。設計を怠ってしまうとすぐに「スパゲッティプログラム」になってしまう。

最大の問題は、密結合になりやすくなること。互いに密に依存している状態であるため、何かしらの変更を加えるだけで予期せぬ影響を及ぼす場合があり、クラスの階層が深くなるほど、コードの複雑性とメンテナンスコストが増加する。

そこで継承の代わりに委譲を使用することで、上記の弊害を防ぐことが可能になる場合がある。

特にクラスの間で明確な 「is-a」関係がない場合 には、継承ではなく委譲を検討するべき。

委譲とは

あるオブジェクトが特定の機能を他のオブジェクトに任せること。委譲を使用することで、先ほど解説した継承による最大の弊害の密結合を避け、より柔軟で再利用可能な設計が可能になる。

継承と違い、委譲は一部機能(プロパティやメソッド)を受け渡し、使用する形

# エンジンクラス
class Engine
  def start
    "Engine started"
  end
end

# カークラス
class Car
  def initialize
    @engine = Engine.new
  end

  def drive
    @engine.start + " and the car is driving"
  end
end

car = Car.new
puts car.drive  # => "Engine started and the car is driving"

CarクラスがEngineクラスのインスタンスをプロパティとして持ち、その機能を利用することで車を運転する動作を実現している。各クラスの責務はそのままに、Engineクラスの機能をCarクラスは使用できる形になっている。機能を委譲されたクラスは独立して存在し、変更が加わっても他のクラスに影響を与えることなく保守することができる状態になっている。

継承と委譲の選択

継承と委譲のどちらを選択すべきかは、クラス間の関係性やそのクラスが持つべき責務に着目すべき

まず、継承はクラス間に明確な「is-a」関係が存在する場合、また子クラスが親クラスの責務をそのまま引き継ぐべき場合に適している。

# 親クラス
class Vehicle
  def start
    "Starting the vehicle"
  end
end

# 子クラス
class Car < Vehicle
  def drive
    "Driving the car"
  end
end

class Bicycle < Vehicle
  def pedal
    "Pedaling the bicycle"
  end
end

car = Car.new
puts car.start  # => "Starting the vehicle"
puts car.drive  # => "Driving the car"

bicycle = Bicycle.new
puts bicycle.start  # => "Starting the vehicle"
puts bicycle.pedal  # => "Pedaling the bicycle"

上記例で言うと、CarBicycleVehicleであるという関係にある。親クラスの共通の機能を子クラスで再利用しつつ、子クラス独自の振る舞いを追加できる状態。

一方、委譲(コンポジション)はクラス間に「has-a」の関係があるとき、また子クラスが親クラスと異なる責務を持つ場合に適している。

# エンジンクラス
class Engine
  def start
    "Engine started"
  end
end

# カークラス
class Car
  def initialize
    @engine = Engine.new
  end

  def drive
    @engine.start + " and the car is driving"
  end
end

car = Car.new
puts car.drive  # => "Engine started and the car is driving"

上記で言うと、CarEngineを「持っている」ので「has-a」の関係にある。このような関係では、CarクラスがEngineクラスをプロパティとして持ち、その機能を必要に応じて委譲する方が適切。

「クラス間に明確な「is-a」関係が存在するか?」

「子クラスがどのような責務を持つべきか?」

上記2点をもとにどちらを使用するべきかを判断するのがひとまずは良いと思う。

まとめ

継承と委譲の選択においては、上記での判断をすることが重要ではある。しかし、実際の開発では、継承には密結合や保守性の低下などの弊害が生じやすいというリスクが伴う。

そのため、委譲の方が適していることが圧倒的に多い。基本的には継承を使用しない形を取っても良いと思っている。継承を使用する場合は、慎重に検討し、その設計が本当に「is-a」関係に基づいており、システム全体の柔軟性を損なわないかどうかを十分に考慮する必要ある。

最終的には、継承を使うか委譲を使うかを選択する際には、委譲の方が安全で適切な選択となるケースが多いことを認識し、慎重に判断することが重要。

Discussion