設計原則とは何か
設計原則とは、変更に強いクラスを作るための、経験から生まれた心得です。代表が、単一責任の原則と、開放閉鎖の原則です。
原則を知ると、直すたびに別の所が壊れるコードを、書く前に見分けられます。このレッスンでは、悪い例を見つけて、直します。
このレッスンのゴール
メモ
仕事を2つ持つクラスや、種類が増えるたびに直すコードを見つけ、分けて直すことができる。
キーワード
| ことば | いみ | れい |
|---|---|---|
| 責任 | クラスが受け持つ仕事の1つのまとまり | |
| 単一責任の原則 | 1つのクラスには、変わる理由を1つだけ持たせる心得 | |
| 開放閉鎖の原則 | 新しい種類は足して加え、今あるコードは直さずに済ませる心得 | |
| コードの臭い | 設計の問題がありそうだと知らせる、コードの形 | elif の連続 |
| リファクタリング | 動きを変えずに、中の作りを整えること |
学生が入力するセルは、このプレビューでは空白です。
1つのクラスは、変わる理由を1つだけ持つ
注文のクラスが、合計の計算と、画面の文面と、ファイルへの保存を全部書いているとします。
文面を変えたい人も、保存先を変えたい人も、同じクラスを開きます。変わる理由が3つあるのです。これを、単一責任の原則に反すると言います。
仕事が3つあるクラス
class Order:
def __init__(self, prices):
self.prices = prices
# 理由1: 合計の計算の決まりが変わる。
def total(self):
return sum(self.prices)
# 理由2: 画面や紙の見た目が変わる。
def to_text(self):
return "合計 " + str(self.total()) + "円"
# 理由3: 保存の場所や形式が変わる。
def save(self, path):
with open(path, "w") as f:
f.write(self.to_text())
例: 注文のクラスが変わる理由
| メソッド | 仕事 | 変わる理由 | 直す人 |
|---|---|---|---|
| total | 合計を計算する | 値引きの決まりが変わる | 業務の担当 |
| to_text | 文面を作る | 表示の形が変わる | デザインの担当 |
| save | ファイルに保存する | 保存先が変わる | 運用の担当 |
学生が入力するセルは、このプレビューでは空白です。
仕事ごとにクラスを分ける
直し方は、変わる理由ごとにクラスを分けることです。Order は計算だけ、OrderText は文面だけ、OrderFile は保存だけを受け持ちます。
分けると、文面を直しても、計算のクラスは開かずに済みます。テストも、仕事ごとに書けます。
3つの仕事を3つのクラスに分ける
class Order:
def __init__(self, prices):
self.prices = prices
def total(self):
return sum(self.prices)
class OrderText:
def render(self, order):
return "合計 " + str(order.total()) + "円"
class OrderFile:
def save(self, text, path):
with open(path, "w") as f:
f.write(text)
開放閉鎖の原則は、足して増やす
運賃を種類で分ける if と elif の関数は、新しい種類のたびに、関数の中を直します。直すたびに、今動いている種類を壊す危険があります。
種類ごとにクラスを作れば、新しい種類はクラスを足すだけです。今あるコードは、直さずに済みます。これが開放閉鎖の原則です。
直す形と足す形の比べ方
# 閉じた形: 種類が増えるたびに、この関数を直します。
def fare(kind, base):
if kind == "child":
return base // 2
elif kind == "senior":
return base * 80 // 100
return base
# 開いた形: 新しい種類は、クラスを足すだけです。
class Student:
def fare(self, base):
return base * 70 // 100
print(Student().fare(600))
ラボで設計の臭いを数える
ラボの端末で、2つのファイルを作り、設計の臭いを数えます。6つの Run を、上から順に押してください。
1つ目のファイルは、仕事が3つのクラスです。2つ目は、elif が続く関数です。
作業用のフォルダを作る
mkdir -p ~/oop/principlesここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
そのフォルダへ移る
cd ~/oop/principlesここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
仕事が3つのクラスを書く
printf 'class Order:\n def __init__(self, prices):\n self.prices = prices\n\n def total(self):\n return sum(self.prices)\n\n def to_text(self):\n return "total " + str(self.total()) + " yen"\n\n def save(self, path):\n with open(path, "w") as f:\n f.write(self.to_text() + "\\n")\n' > order.pyここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
クラスのメソッドを並べる
grep -n "def " order.pyここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
メソッドの一覧が、仕事の一覧になる
出力は、__init__、total、to_text、save の4行で、行の番号が付きます。
total は計算、to_text は文面、save は保存です。1つのクラスに、3つの仕事があると、一覧から分かります。
種類で分かれる関数を書く
printf 'def fare(kind, base):\n if kind == "adult":\n return base\n elif kind == "child":\n return base // 2\n elif kind == "senior":\n return base * 80 // 100\n return base\n\n\nprint(fare("child", 600))\n' > smell.pyここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
elif の行を数える
grep -c elif smell.pyここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
elif の数は、種類の数に合わせて増える
出力は 2 です。3つの種類を分けるのに、elif が2行あります。
種類が1つ増えるたびに、この数も1つ増えます。数が増え続けるなら、クラスに分けるべき合図です。
よくある間違い
注意
「単一責任とは、メソッドを1つしか持たないことだ」と考える人が多くいます。それは間違いです。1つのクラスに、メソッドがいくつあっても構いません。変わる理由が1つなら、守れています。