🚀 ニフティ’s Notion

【オブジェクト指向2024 #4】お題: サブスク管理システムを作ろう

サブスクを管理するクラスを作ることになった

偉い人: 今作ってるサブスク管理システムにさ、申し込んだプランIDと月額価格とを管理するクラス追加しといてよ
A: わかりました!すぐやります!
A: いい感じにカプセル化して、…こんな感じかな
class Subscription:
    def __init__(self, plan_id: int, price: int):
        self.__plan_id = plan_id
        self.__monthly_price = price

    def plan_id(self):
        return self.__plan_id

    def monthly_price(self):
        return self.__monthly_price

割引機能の追加が要望される

偉い人: 昨日のシステムだけどさ、キャンペーンすることもあるらしいんだよね。だから
  • 20%オフとかそういうのときの割引後の金額で計算できるようにしといてよ。
  • あと具体的にお得になった割引額も計算できるといいな
A: うーん、やってみます…
A: カプセル化しておいてよかった。ゲッターで計算すればいいや
class Subscription:
    # ...

    def monthly_price(self):
        "割引後の価格を計算する"
        return int(self.__monthly_price * (1 - self.__discount_rate))

    def rate_discount(self, discount_rate: float):
        "割引率を設定する"
        self.__discount_rate = discount_rate

    def compute_discounted_amount(self):
        "割引される額を計算する"
        return self.__monthly_price - self.monthly_price()


sub = Subscription(plan_id=12345, price=1000)  # 1000円のプランを購読
sub.rate_discount(0.2)  # 20% OFF
print(sub.monthly_price())  # => 800円

固定値の値引き機能の追加も要望される

偉い人: 例の値引きだけど、割合で減らすだけじゃなくて固定値の50円引きとかもやるらしいのよ。どっちでも対応できるようにしといて〜
A: えっっっ…。だいぶ辛くなってきたかも
class Subscription:
    # ...

    def monthly_price(self):
        "割引後の価格を計算する"
        # 割引方法が二種類あるので条件分岐
        match self.__discount_type:
            case "rate":
                # 割引なので self.__discount_rate を元に計算
                return int(self.__monthly_price * (1 - self.__discount_rate))
            case "value":
                # 固定値値引きなので self.__discount_value を元に計算
                return self.__monthly_price - self.__discount_value
            case _:
                raise RuntimeError()

    def rate_discount(self, discount_rate: float):
        "割引率を設定する"
        self.__discount_type = "rate"
        self.__discount_rate = discount_rate

    def value_discount(self, discount_value: int):
        "割引額を設定する"
        self.__discount_type = "value"
        self.__discount_value = discount_value

    # ...


sub = Subscription(plan_id=12345, price=1000)  # 1000円のプランを購読
sub.rate_discount(0.2)  # 20% OFF
print(sub.monthly_price())  # => 800円

sub = Subscription(plan_id=12345, price=1000)  # 1000円のプランを購読
sub.value_discount(300)  # 300円 OFF
print(sub.monthly_price())  # => 700円

依頼はもっと複雑に…

その日の晩、Aさんはこんな夢を見ました。

偉い人: ありがとう!おかげで柔軟にキャンペーンを打てて契約数もうなぎのぼりだ。ここからさらに
  • 割引と固定値値引きを順番に適用するとか、
  • 初月無料のキャンペーンとか、
  • セット割で一定期間だけ値引き値や割合の増額とか、

そういうの実装してくれないかな。

A: ミ゜

なにがよくなかったのか

こいつがじわじわ効いてきています。

  def monthly_price(self):
      "割引後の価格を計算する"
      # 割引方法が二種類あるので条件分岐
      match self.__discount_type:
          case "rate":
              return int(self.__monthly_price * (1 - self.__discount_rate))
          case "value":
              return self.__monthly_price - self.__discount_value
          case _:
              raise RuntimeError()
  1. サブスクを管理するクラスの中にキャンペーンのロジックまで混ぜ込もうとしてしまったこと。
    Icon
    実は、これは後で説明するSOLID原則の「S: 単一責任の原則」に対する違反です。
  2. いろいろな種類のキャンペーンを一つのクラス内の条件分岐で頑張ろうとしたこと。
    Icon
    実は、これは後で説明するSOLID原則の「S: 単一責任の原則」に加え、「O: オープン・クローズドの原則」に対する違反でもあります。

どうすればよかったのか

→ クラスを分ける。

  • サブスクそのものを管理するクラス Subscription
  • 割引等のキャンペーンを管理するクラス RateDiscountCampaign / ValueDiscountCampaign

次: 📄 【オブジェクト指向2024 #5】処理の見通しを良くするには