メール送信インターフェースを作成する
  
  
    
      
        
          
            Python では、
          
        
      
    
  
  
    
      
        
          
            ABC (Abstract Base Class)
          
        
      
    
  
  
    
      
        
          
            という特別な基底クラスを継承した親クラスを作ると、インターフェースのようなものを定義することができる。
          
        
      
    
  
  
  
  
    
      
        
          
            メール送信のインターフェース 
          
        
      
    
  
  
    
      
        
          
            MailClient
          
        
      
    
  
  
    
      
        
          
             を定義するときはこんな感じ。
          
        
      
    
  
  
from abc import ABC, abstractmethod
# MailClient インターフェース。
#
# メールの送信ができることを規定する。
class MailClient(ABC):
    @abstractmethod
    def send_mail_to(self, address: str, message: str):
        "メール送信処理"
        pass
        
  本番環境用の MailClient を作る
これを実装 (Python の仕組み上は継承) するクラスを追加すると、そのクラスが「メール送信機能を持つ」という意思表示ができる。
# 本番環境用
class ActualMailClient(MailClient):
    @override
    def send_mail_to(self, address: str, message: str):
        # 本当はメール送信処理、例示のため print() で代用しておく
        print("actually send mail")
        pass
        
  
  
  
    
      
        
          
            そして、 
          
        
      
    
  
  
    
      
        
          
            subscribe()
          
        
      
    
  
  
    
      
        
          
             関数がクライアントを受け取るようにすれば、本番はこのように実装できる。
          
        
      
    
  
  
def subscribe(mail_client: MailClient, mail_address: str, plan_id: int):
    "申し込む"
    # ...なんやかんや申込処理...
    # メール送信
    message = f"{subscription.monthly_price()}円で購読ありがとうございます"
    mail_client.send_mail_to(mail_address, message)
# 本番
subscribe(ActualMailClient(), "test@example.com", 12345)
        
  テスト環境用の MailClient を作る
ではテストは?
…テスト用に、何もしない MailClient を作成してあげればいい。
ポリモーフィズムにより、subscribe() 関数は何も修正する必要はない。
# テスト用
class MockMailClient(MailClient):
    @override
    def send_mail_to(self, address: str, message: str):
        # テスト用なので、メール送信をしない
        pass
# テスト用
subscribe(MockMailClient(), "test@example.com", 12345)
        
  ポイント: なるべく小さいインターフェースにする
同じ種類の操作は、バラバラにするよりなるべく同じインターフェースにまとめたほうが意味上のつながりを理解しやすくなり、可読性や変更容易性を高めることになる。
だが一方で、何もかもを同じインターフェースにいれるのもまた問題を起こしてしまう。
例えば、メール送信インターフェースの中に、メール送信取り消しメソッドも含まれていたとしよう。
そんな機能普通つかわないよねというツッコミはナシでお願いします。
例えば… サブスク管理システムの中で、メール送信機能が次の2つのユースケースで使われるとしよう。
- 社内に日次申込統計を送信する
 - お客様にありがとうメールを送る
 
日次申込統計の送信後、誤りが発覚した場合に取り消したいケースを考えよう。同一組織内ならメール送信取り消しができるメールサービスもあるらしいので、社内のメール送信である日次申込統計の送信では念の為キャンセルできるようにしておきたい。メール関連処理なので MailClient インターフェースに取り消しメソッドを追加すれば良いように思える。
ところが、ごく少数の例外を除けば、基本的にメール配信サービスでは取り消しはできない。
だがインターフェースに含まれてしまっていると形式的には子クラスで実装しないといけないことになる。
すると苦肉の策として、呼び出されたら例外を投げるだけの実装が爆誕する。
class MailClient(ABC):
    @abstractmethod
    def send_mail_to(self, address: str, message: str):
        "メール送信処理"
        pass
    @abstractmethod
    def cancel_sent_mail(self, address: str):
        "送信取り消し処理"
        pass
class WorldWideMailClient(MailClient):
    "大規模メール配信サービスを利用するクライアント"
    # ...
    @override
    def cancel_sent_mail(self, address: str):
        raise RuntimeError("組織外に送ったメールは取り消せません")
class OrgMailClient(MailClient):
    "組織内メールサービスを利用するクライアント"
    # こちらはフル機能で実装できる
    # ...
        
  具体的にどのレベルの処理までまとめてもいいのか、には答えがない。状況に応じて考えることが必要になる。
まとめ
- インターフェースを使うと、「クラスができること」を規定することができる
 - 直接具体的な処理内容に依存するのではなく「できること」にだけ依存することで、具体的な処理を差し替えることができるようになり、再利用性が高まる
 - 同じ意味の処理はまとめたほうがよい反面、まとめすぎると問題を起こすこともある
 
ここまでみてきたコードの全体像
気が向いたら眺めてみてください。
ファイル>ノートブックをアップロード より以下ファイルを読み込んでください
- リファクタ前のコード
 
- リファクタ後のコード
 
補足
参考: 【徹底解説まとめ】 継承、抽象クラスとインターフェイスの違い、ついでにSOLID【Java】
ABCの挙動
from abc import ABC, abstractmethod
class Animal(ABC):
    @abstractmethod
    def speak(self):
        # これはサブクラスでの実装が必須
        pass
    # @abstractmethodが付いていない通常のメソッド
    def sleep(self):
        print("ぐーぐー...") # デフォルトの実装
# --- サブクラス ---
class Dog(Animal):
    def speak(self):
        return "ワン!"
    # sleepはオーバーライドしない -> Animalのデフォルト実装が使われる
class Cat(Animal):
    def speak(self):
        return "ニャー"
    # sleepをオーバーライドする
    def sleep(self):
        print("すぴー...") # Cat独自の実装
# 抽象メソッドを実装していないクラス(エラーになる例)
# class Bird(Animal):
#     def fly(self):
#         return "パタパタ"
# --- 実行 ---
my_dog = Dog()
print(my_dog.speak()) # => ワン!
my_dog.sleep()      # => ぐーぐー... (Animalのデフォルト実装)
my_cat = Cat()
print(my_cat.speak()) # => ニャー
my_cat.sleep()      # => すぴー... (Catでオーバーライドした実装)
# Birdクラスのコメントを外してインスタンス化しようとするとエラーになる
# my_bird = Bird() # TypeError: Can't instantiate abstract class Bird with abstract method speak
# Animal自体はインスタンス化できない
# animal = Animal() # TypeError: Can't instantiate abstract class Animal with abstract method speak