キャンペーンをどう扱うべきだったか?
ロジックが複雑になっても見通しよく実装するためには、値引きを計算する処理は別のクラスに切り出してしまうのがよい。例えば割引ならこう。
class RateDiscountCampaign:
    def __init__(self, discount_rate: float):
        self.__discount_rate = discount_rate
    def get_discounted_price(self, price: int):
        "割引後の価格を計算する"
        return int(price * (1 - self.__discount_rate))
    def compute_discounted_amount(self, price: int):
        "割引される額を計算する"
        return price - self.get_discounted_price(price)
        
  固定値の値引きも同様に切り出そう。
class ValueDiscountCampaign:
    def __init__(self, discount_value: float):
        self.__discount_value = discount_value
    def get_discounted_price(self, price: int):
        "割引後の価格を計算する"
        return price - self.__discount_value
    def compute_discounted_amount(self, price: int):
        "割引される額を計算する"
        return price - self.get_discounted_price(price)
        
  個々のキャンペーンクラスを使って金額を計算する
class Subscription:
    def __init__(self, plan_id: int, price: int, campaign):
        self.__plan_id = plan_id
        self.__monthly_price = price
        self.__campaign = campaign
    def plan_id(self):
        return self.__plan_id
    def monthly_price(self):
        "割引後の価格を計算する"
        return self.__campaign.get_discounted_price(self.__monthly_price)
    def compute_discounted_amount(self):
        "割引される額を計算する"
        return self.__campaign.compute_discounted_amount(self.__monthly_price)
        
  前のにくらべて なんて美しいコードなんだ…
ロジックの複雑性をあげていた条件分岐がなくなり、個別の割引ロジックもなくなってかなりきれいにできた。
でも...
まだ気になるところもある。
campaign にどんな値でも渡せてしまう
class Subscription:
    def __init__(self, plan_id: int, price: int, campaign):
        #                     ^^^^^       ^^^^^  -------- 何もついてない
        
  このような定義だと実は campaign にどんな値でも渡すことができてしまうので、なんか変なものを渡す人が出てくるかもしれない。たとえばただの文字列とかも何も文句なく渡されてしまう。
sub = Subscription(
    plan_id=12345,
    price=1000,  # 1000円のプランを購読
    campaign="campaign",  # ???
)
print(sub.monthly_price())  # AttributeError! 例外
        
  こんな想定外なものが来るともちろん例外になってしまう。
Traceback (most recent call last):
  File "/Users/sci02183/workspace/daily/2024/0514/junk_183858.py", line 67, in <modul
e>
    print(sub.monthly_price())
          ^^^^^^^^^^^^^^^^^^^
  File "/Users/sci02183/workspace/daily/2024/0514/junk_183858.py", line 41, in monthl
y_price
    return self.__campaign.get_discounted_price(self.__monthly_price)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AttributeError: 'str' object has no attribute 'get_discounted_price'
        
  
  
  
    
      
        
          
            だからといって、 
          
        
      
    
  
  
    
      
        
          
            campaign: RateDiscountCampaign
          
        
      
    
  
  
    
      
        
          
             のように指定してしまうと…
          
        
      
    
  
  
class Subscription:
    def __init__(self, plan_id: int, price: int, campaign: RateDiscountCampaign):
        # ...
        
  
  
  
    
      
        
          
            ValueDiscountCampaign
          
        
      
    
  
  
    
      
        
          
             が渡せなくなって、困る。
          
        
      
    
  
  
        
        
  
    
      
        
          
            compute_discounted_amount()
          
        
      
    
  
  
    
      
        
          
             関数の複製
          
        
      
    
  
      
    全く同じ処理なのに、別のクラスになったことで2つに分裂してしまった。こうなってしまうと、後々何か修正が入った際に修正漏れを起こすリスクが生まれてしまう。
class RateDiscountCampaign:
    # ...
    def compute_discounted_amount(self, price: int):
        "割引される額を計算する"
        return price - self.get_discounted_price(price)
        
class ValueDiscountCampaign:
    # ...
    def compute_discounted_amount(self, price: int):
        "割引される額を計算する"
        return price - self.get_discounted_price(price)
        
  解決するには
これを解決するにはどうすればよいのだろう?
私たちが表現したいことは、
- campaign には何かしらの「キャンペーン」をセットしてほしいということ、
 - キャンペーン内容がわかれば割引額は共通の方法で計算できる (compute_discounted_amount) ということ。
 
人間ならざっくりとした感覚で、割引や固定値値引きはどちらもキャンペーンであると思えるが、コンピュータに対してはそれを明確にコード上で表現してあげる必要がある。
そのための仕組みが「継承」と呼ばれている。