継承より委譲を選ぶ
コンポジションとは、クラスが別のオブジェクトを部品として持ち、仕事の一部をその部品に任せる設計です。継承の代わりになる、もう1つの組み立て方です。
部品を取り替えるだけで動きが変わるので、組み合わせが増えても、クラスの数が爆発しません。
このレッスンのゴール
メモ
継承ではなくコンポジションを選ぶ理由を言い、部品を取り替える設計を書くことができる。
キーワード
| ことば | いみ | れい |
|---|---|---|
| コンポジション | オブジェクトを部品として持って組み立てる設計 | |
| 委譲 | 自分で計算せず、持っている部品に仕事を任せること | self.tax.add(x) |
| has-a 関係 | 「AはBを持っている」と言える関係 | |
| is-a 関係 | 「AはBの一種だ」と言える関係。継承の形 | |
| 部品の取り替え | 持っている部品を、同じ呼び方ができる別の部品に替えること |
学生が入力するセルは、このプレビューでは空白です。
継承だけだと、クラスが組み合わせの数だけ増える
レシートに、税の種類と、割引の種類があるとします。継承で作ると、「10%の税で学生割引」のように、組み合わせごとにクラスが要ります。
税が3種類、割引が3種類なら、9つです。1つ増えるたびに、さらに3つ増えます。
例: 継承と部品の、クラスの数
| 税の種類 | 割引の種類 | 継承で作るクラス | 部品で作るクラス |
|---|---|---|---|
| 3 | 3 | 9 | 6(税3と割引3) |
| 4 | 3 | 12 | 7(税4と割引3) |
| 4 | 5 | 20 | 9(税4と割引5) |
学生が入力するセルは、このプレビューでは空白です。
クラスが部品を持てば、仕事を任せられる
レシートは、税の計算そのものを自分で書きません。税の部品を1つ持ち、計算はその部品に頼みます。これが委譲です。
「レシートは税を持っている」と言えます。これが has-a 関係です。「レシートは税の一種」とは言えないので、継承は合いません。
税の部品を持つレシート
class TenPercent:
def add(self, yen):
return yen * 110 // 100
class EightPercent:
def add(self, yen):
return yen * 108 // 100
class Receipt:
def __init__(self, tax):
# 税の計算を、部品として1つ持ちます。
self.tax = tax
def total(self, prices):
subtotal = 0
for price in prices:
subtotal = subtotal + price
# 税の計算は、部品に任せます。
return self.tax.add(subtotal)
receipt = Receipt(TenPercent())
print(receipt.total([100, 250]))
# 次の行で、部品を取り替えます。
receipt.tax = EightPercent()
print(receipt.total([100, 250]))
部品は動いている最中にも取り替えられる
継承は、クラスを書いた時点で決まります。作ったオブジェクトを、後から別の種類に変えることはできません。
部品は属性なので、プログラムの動いている最中に取り替えられます。軽減税率の日だけ、税の部品を替える、といった使い方ができます。
ラボで部品を取り替える
ラボの端末で、税の部品とレシートのファイルを作り、1行ずつ動かします。5つの Run を、上から順に押してください。
同じレシートが、部品しだいで違う合計を返すのを確かめます。
作業用のフォルダを作る
mkdir -p ~/oop/compositionここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
そのフォルダへ移る
cd ~/oop/compositionここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
部品とレシートを書く
printf 'class TenPercent:\n def add(self, yen):\n return yen * 110 // 100\n\n\nclass EightPercent:\n def add(self, yen):\n return yen * 108 // 100\n\n\nclass Receipt:\n def __init__(self, tax):\n self.tax = tax\n\n def total(self, prices):\n subtotal = 0\n for price in prices:\n subtotal = subtotal + price\n return self.tax.add(subtotal)\n' > parts.pyここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
10%の税で合計する
python3 -c "from parts import *; print(Receipt(TenPercent()).total([100, 250]))"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
部品を取り替えて合計する
python3 -c "from parts import *; r = Receipt(TenPercent()); r.tax = EightPercent(); print(r.total([100, 250]))"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
同じ合計の関数が、違う答えを返す
1つ目の出力は 385、2つ目は 378 です。値段の合計は、どちらも350円です。
350円に10%の税を足すと385円、8%なら378円です。Receipt は書き換えていません。部品だけを替えました。
よくある間違い
注意
「動きを変えるには、サブクラスを作るしかない」と考える人が多くいます。変えたい所が1つの部品に切り出せるなら、部品を取り替えるほうが簡単です。継承は、本当に is-a と言えるときに選びましょう。