この記事では、オブジェクト指向における継承と委譲の違いについて解説します。
継承と委譲の違い
継承と委譲の違いは「他のクラスの機能をどう使うか?」という点にあります。
継承では、親クラスのすべての機能を子クラスが引き継ぎます。
委譲では、クラスの一部の機能の実装を他のクラスに任せることになります。
継承では、あるクラスから別のクラスにプロパティやメソッドを引き継ぎます。
委譲では、あるクラスから別のクラスの一部のプロパティやメソッドを使用します。
継承と委譲をサンプルコードで比較
サンプルコードを使って継承と委譲の違いを詳しく説明しましょう。
以下は、親クラスの機能を子クラスに継承する例です。
class Car:
def run(self):
# 車を走らせる
def charge(self):
# 燃料を補給する
class ElectricCar(Car): # Carクラスを継承
def run(self):
# 電気自動車を走らせる
def charge(self):
# バッテリーを充電する
親クラスであるCarクラスをElectricCarクラスが継承しています。
次は、あるクラスのメソッドを別のクラスに委譲する例です。
class Engine:
def start(self):
# エンジンを始動する
def stop(self):
# エンジンを停止する
class Car:
def __init__(self):
self.engine = Engine() # エンジンクラスのインスタンスを保持する
def run(self):
self.engine.start() # 「エンジンの始動」の機能はEngineクラスに委譲する
# 車の走行に関する処理
def charge(self):
self.engine.stop() # 「エンジンの停止」の機能はEngineクラスに委譲する
# 燃料の補給に関する処理
CarクラスがEngineに関する処理の実装をEngineクラスに委譲しています。CarクラスでEngineクラスのメソッドを呼び出す形になっています。
このように、継承では子クラス(機能を引き継ぐクラス)が親クラスの機能をすべて引き継ぐのに対し、委譲では一部のメソッドを別のクラスから呼び出します。
継承ではすべての機能を、委譲では一部の機能を他のクラスに受け渡すというわけです。
継承と委譲の使い分け
あるクラスの機能を別のクラスで使いたいとき、継承と委譲のどちらを使うかはとても難しい問題です。
どちらを使うかがプログラムの保守性や拡張性に大きく影響します。
継承と委譲は「子クラスがどのような責務を持つべきか?」によって使い分けます。
「責務」とは「そのクラスが果たすべき役割」を意味します。
たとえば、以下のCarクラスは「走る役割(run)」と「ガソリンを補給する役割(charge)」を持っています。これがCarクラスの責務です。
class Car:
def run(self):
# 車を走らせる
def charge(self):
# 燃料を補給する
継承を使うタイミング
ここでCarクラスの機能を持ったElectricCarクラスを作りたいとします。
このとき、Carクラスの機能を「継承」すべきでしょうか?「委譲」すべきでしょうか?
電気自動車は車の一種なので、ElectricCarクラスはCarクラスと同様に「走る役割(run)」と「ガソリンを補給する役割(charge)」を期待されています。
このように親クラスと子クラスが「同じ役割を期待されている=同じ責務を持つべき」場合は、継承を使います。
class ElectricCar(Car):
def __init__(self, battery_level=100):
self.battery_level = battery_level
def run(self):
if self.battery_level > 0:
print("走行します。")
self.battery_level -= 10
else:
print("バッテリーが切れています。")
def charge(self):
self.battery_level = 100
print("バッテリーを充電しました。")
ElectricCarクラスはCarクラスを継承しているので、Carクラスと同じ責務を果たすことができます。
ElectricCarクラスを使う側は、Carクラスと同様にrunメソッドとchargeメソッドを呼び出すことができます。
このように子クラスが親クラスと同じ責務を持つべきときは継承を使います。
委譲を使うタイミング
続いて、委譲を使う場合を考えます。
委譲では、他クラスの一部の機能が引き継がれますが、責務は引き継がれません。
子クラスは親クラスの一部の機能をツールのように使用するだけです。
ここではエンジンの役割を果たすEngineクラスを考えます。
class Engine:
def start(self):
# エンジンを始動する
def stop(self):
# エンジンを停止する
Engineクラスには「エンジンを始動する役割(start)」と「エンジンを停止する役割(stop)」があります。
ここでEngineクラスを使って、自動車であるCarクラスを作りたいとしましょう。
このとき、CarクラスはEngineクラスを継承すべきでしょうか?
それともCarクラスの機能の実装をEngineクラスに委譲すべきでしょうか?
Engineクラスの責務は「エンジンの始動」と「エンジンの停止」です。
それはエンジンに期待される役割であって、自動車の役割ではありません。
そのため、CarクラスはEngineクラスの責務を引き継ぐべきではありません。
「エンジンの始動」と「エンジンの停止」の責務はEngineクラスが担い、CarクラスはEngineクラスの機能をツールのように利用すべきです。
したがって、Carクラスの機能の実装をEngineクラスに委譲するのが適切です。
class Car:
def __init__(self):
self.engine = Engine() # エンジンクラスを利用する
def run(self):
self.engine.start() # 「エンジンの始動」の機能はEngineクラスに委譲する
# 車の走行に関する処理
def charge(self):
self.engine.stop() # 「エンジンの停止」の機能はEngineクラスに委譲する
# 燃料の補給に関する処理
継承と委譲の使い分けについてのまとめ
継承と委譲の使い分けについてまとめると以下になります。
- 継承は、他のクラスの責務も引き継ぐときに使う。
- 委譲は、責務は他のクラスに持たせたまま、一部の機能の実装を他のクラスに任せるときに使う
最近のシステム開発の現場では、継承を使うべきタイミングはあまり多くありません。継承では親クラスの機能と責務をすべて引き継ぐため、上手く使わないと保守コストが高まるためです。
一方の委譲では、クラスごとに責務をきちんと分離できるため保守コストが下がりやすく、複数チームを横断する開発でも使いやすい傾向にあります。
インターフェース
継承は親クラスの機能と責務を子クラスに引き継ぐ機能であり、委譲はクラスの実装の一部を他のクラスに任せる機能でした。
実は継承の他にも、クラスに責務を強制する方法があります。
インターフェースです。
インターフェースとは、クラスに対して特定の機能の実装を強制する機能です。
言葉だけでは説明が難しいためコードを使って例を示します。
以下は、車クラスが満たすべき機能の実装を強制するインターフェースです。
from abc import ABC, abstractmethod
class Car(ABC):
@abstractmethod
def start(self):
pass
@abstractmethod
def stop(self):
pass
@abstractmethod
def accelerate(self):
pass
@abstractmethod
def brake(self):
pass
@abstractmethod
def get_speed(self):
pass
PythonではABCクラスを継承するとともに@abstractmethodデコレータでメソッドを修飾することによってインターフェースを定義できます。
インターフェース自体には機能を実装しません。
インターフェースはクラスに適用する形で使用します。
具体的なクラスをインターフェースにしたがって実装するわけです。
ある種のテンプレートのようなものと考えるとイメージしやすいかもしれません。
インターフェースを実装することを宣言したクラスは、インターフェースに定義されているすべての機能を実装しなくてはいけません。
ここで先ほどの車クラスのインターフェースを実装した具体的な車クラスを書いてみましょう。
class SportsCar(Car):
def start(self):
# スポーツカーを起動する処理を実装する
pass
def stop(self):
# スポーツカーを停止する処理を実装する
pass
def accelerate(self):
# スポーツカーを加速させる処理を実装する
pass
def brake(self):
# スポーツカーをブレーキする処理を実装する
pass
def get_speed(self):
# 現在のスポーツカーの速度を取得する処理を実装する
pass
インターフェースで定義したすべてのメソッドをクラスに実装しています。
またメソッドの中身も実装しています。
このようにインターフェースを使えばクラスに対して特定の機能の実装を強制できます。
インターフェースの使いどころ
インターフェースの用途はクラスに機能の実装を強制することです。
ライブラリやフレームワーク、巨大なシステムなどを開発していると、クラスに対して機能の実装を強制したい場面が出てきます。
たとえば、WEBフロントエンドフレームワークであるAngularのComponentの実装では以下のようなインターフェースが使われています。
@Component({selector: 'my-cmp', template: `...`})
class MyComponent implements OnInit {
ngOnInit() {
// ...
}
}
「implements ngOnInit」でインターフェースの実装を宣言します。
ngOnInitはコンポーネントの初期化処理を実装するためのインターフェースです。
これを書くとMyComponentクラスではngOnInitメソッドの実装を強制されます。もしngOnInitメソッドを実装しなければエラーが発生します。
このように、フレームワークでは実装者にインターフェースを提供することで使い方を制限しています。
実装の制約を強めることで、実装の効率性や安全性を高めることができるのです。
継承と委譲のまとめ
この記事では、継承と委譲の違いを解説しました。
継承と委譲の使い分けをマスターするとシステムの設計力が大幅にアップします。
いろいろなパターンで使って使いどころを覚えましょう。