SOLID原則:リスコフの置換原則
リスコフの置換原則とは?
- Liskov Substitution Principle = LSP
- あるクラスを別のクラスが継承する 文脈で関係してくる原則
- 親の決まりを子供は破ってはいけないの原則
- (大半が難しい文章で説明されていて、個人的には上の文章がしっくりきた ✨)
クラスを継承するとき、
継承元 = 親クラス
継承する側 = 子クラス
という場合があると思いますが、この時に、継承する側である子クラス内で、親クラスで定められているルールを破るコードを書いてはいけない というのがリスコフの置換原則
別の言い方をすると、
親クラスを子クラスで置き換えても問題なく動作する とも言える
よくない例
- 実際にサンプルコードを書いてリスコフの置換原則を破ってみる
- Pythonで書いてます
- 以下はとあるチームでの開発を例として取り上げてみた
〜とあるチームの開発初期の話〜
- ある店舗に並べる商品を定義するクラスが必要になり以下のクラスを作成
# 店舗に並ぶ商品を宣言したクラス class Goods: name: str # 仕入れ原価 price: int def get_price(self) -> int: """商品の値段を返すメソッド.""" return self.price def set_price(self, price: int) -> none: """商品に値段をつけるメソッド.""" self.price = price
〜消費税を導入〜
- 拡張機能で消費税10%を考慮する必要が出てきた
- もともと店舗に並ぶ商品を宣言したクラスを継承して、新しいクラスを作ることにした
class NewGoods(Goods): def set_price(self, price: int) -> None """商品に値段をつけるメソッド.""" self.price = int(price * Decimal('1.10'))
- 親クラスである
Goods
クラスの値段設定メソッドは、引数で渡された値をそのまま商品の値段として設定するだけの機能だった - しかし、子クラス(
NewGoods
クラス)では、引数で渡された値より10%増の値段が設定される機能になってしまった・・・・ - また
price
変数は仕入れ原価なのに、仕入れ原価でもない変数になってしまった(そもそも変数名分かりづらい問題もある)
=>子クラスが親クラスのルールを守っていない
〜買い物カゴに入っている商品の合計を算出する機能を追加する〜
しばらくして、買い物カゴに入っている商品の合計値を算出して請求金額を出してほしいとの要望があり、新しくコードを実装することになった
商品の宣言は、
NewGoods
クラスだから、このクラスにあるprice
変数を商品ごとに取得してきて、全て足し算すればいいね- と教えてもらった担当者は早速以下のコードを実装。
class CalcGoods: def calc(self, goods_list: list[NewGoods]) -> int: """買い物カゴに入っている商品の合計金額を算出する.""" total_price: int = 0 # わかりやすくfor文で書いておく for goods in goods_list: # NewGoodsはGoodsクラスを継承しているので、get_priceメソッドを使用できる # 消費税考慮 <--- ❌ total_price += int(goods.get_price() * Decimal('1.10')) return total_price
CalcGoods
クラスのcalc
メソッドは、二重で消費税をかけることになり、テストかもしくは本番環境で期待した値段よりも高い値段が算出されてしまうNewGoods
クラスでprice
には商品原価を登録しておく というルールを無視したことが原因で起こってしまったバグ
参考:http://marupeke296.com/OOD_No7_LiskovSubstitutionPrinciple.html
リスコフの置換原則を守るメリットは?
- 今回の例からも言えるが、バグの可能性が減る
子クラス(サブクラス、継承元クラス)を使うときに、意識すべきことが少ない
=>今回の場合だと、
NewGoods
クラスを使うときに、「set_price
メソッドを使う時は、消費税が既に反映されている」という事実を意識して、実装しなければならない という状態が起こっている
うむ、なんとなく理解できた気がする
調べてると、契約による設計 みたいな概念も出てきましたが、
これはまたの機会にしようかなと思います 🌻