Python テンプレートメソッドパターン

テンプレートメソッドパターンは、ある操作におけるアルゴリズムの骨格を定義し、いくつかのステップをサブクラスに遅延させる動作デザインパターンです。テンプレートメソッドにより、サブクラスはアルゴリズムの構造を変更せずに、アルゴリズムの特定のステップを再定義できます。

簡単に言えば、料理のレシピのようなものです。レシピは料理の手順(野菜を洗う、切る、炒める、盛り付ける)を決めますが、具体的にどう洗うか、どう切るか、どう炒めるかは、各シェフが自分のスタイルで実装できます。


なぜテンプレートメソッドパターンが必要なのか?

コードの再利用

テンプレートメソッドパターンは、不変の振る舞いをスーパークラスに移動し、サブクラス内のコード重複を防ぎます。複数のサブクラスは、同じテンプレートメソッドが定義するアルゴリズム構造を共有できます。

拡張性

サブクラスは、フックメソッドや具体的なステップメソッドをオーバーライドすることで、アルゴリズムの全体的な構造を変更することなく、アルゴリズムの部分的な動作を拡張または変更できます。

制御の反転(IoC)

親クラスがアルゴリズムの実行フローを制御し、サブクラスは自分が実装する必要のある特定のステップだけに集中すればよい。これにより「ハリウッドの原則」—「こちらから電話するまで、電話しないでください」—が実現されます。


テンプレートメソッドパターンの構造

クラス図を使って、テンプレートメソッドパターンの構造を理解しましょう。

主要コンポーネントの説明

コンポーネント 責任 説明
AbstractClass アルゴリズムの骨格を定義する テンプレートメソッドと抽象ステップメソッドを含む
ConcreteClass 具体的なステップを実装する 親クラスで定義された抽象メソッドを実装する
template_method テンプレートメソッド アルゴリズムの不変部分を定義する
step1, step2 抽象メソッド サブクラスでの実装が必要な具体的なステップ
hook フックメソッド オプションのステップであり、サブクラスはオーバーライドするかどうかを選択できる

基本構文と実装

抽象基底クラスの定義

例

from abc import ABC, abstractmethod

class AbstractClass(ABC):
    """テンプレートメソッドパターンの抽象基底クラス"""
   
    def template_method(self):
        """テンプレートメソッド - アルゴリズムの骨格を定義"""
        self.step1()
        self.step2()
        self.hook()
        self.step3()
   
    @abstractmethod
    def step1(self):
        """抽象メソッド1 - サブクラスで実装する必要がある"""
        pass
   
    @abstractmethod
    def step2(self):
        """抽象メソッド2 - サブクラスで実装する必要がある"""
        pass
   
    def step3(self):
        """具体メソッド - デフォルト実装がある"""
        print("ステップ3を実行 - デフォルト実装")
   
    def hook(self):
        """フックメソッド - オプションのステップ、サブクラスはオーバーライドを選択できる"""
        print("フックメソッドを実行 - デフォルトでは何もしない")

具体的なサブクラスの実装

例

class ConcreteClassA(AbstractClass):
    """具体的な実装クラスA"""
   
    def step1(self):
        print("ConcreteClassA - ステップ1を実行")
   
    def step2(self):
        print("ConcreteClassA - ステップ2を実行")
   
    def hook(self):
        print("ConcreteClassA - フックメソッドをオーバーライドし、追加機能を追加")

class ConcreteClassB(AbstractClass):
    """具体的な実装クラスB"""
   
    def step1(self):
        print("ConcreteClassB - ステップ1を実行")
   
    def step2(self):
        print("ConcreteClassB - ステップ2を実行")
   
    # hookメソッドをオーバーライドせず、デフォルト実装を使用する

実際の適用例

いくつかの実際の例を通して、テンプレートメソッドパターンの応用を深く理解しましょう。

例1:データ処理のテンプレート

例

from abc import ABC, abstractmethod
import json
import csv

class DataProcessor(ABC):
    """データ処理テンプレート"""
   
    def process_data(self, input_file, output_file):
        """データ処理テンプレートメソッド"""
        print(f"データ処理を開始: {input_file} -> {output_file}")
       
        # データを読み込む
        data = self.read_data(input_file)
        print(f"{len(data)} 件のデータを読み込みました")
       
        # データを変換する
        transformed_data = self.transform_data(data)
        print("データ変換が完了しました")
       
        # データを保存する
        self.save_data(transformed_data, output_file)
        print("データ保存が完了しました")
       
        # クリーンアップ作業
        self.cleanup()
   
    @abstractmethod
    def read_data(self, file_path):
        """データ読み込み - 抽象メソッド"""
        pass
   
    @abstractmethod
    def transform_data(self, data):
        """データ変換 - 抽象メソッド"""
        pass
   
    def save_data(self, data, file_path):
        """データ保存 - 具体メソッド"""
        with open(file_path, 'w', encoding='utf-8') as f:
            if isinstance(data, list):
                for item in data:
                    f.write(str(item) + '\n')
            else:
                f.write(str(data))
   
    def cleanup(self):
        """クリーンアップ - フックメソッド"""
        print("クリーンアップ完了")

class JSONProcessor(DataProcessor):
    """JSONデータプロセッサ"""
   
    def read_data(self, file_path):
        with open(file_path, 'r', encoding='utf-8') as f:
            return json.load(f)
   
    def transform_data(self, data):
        # 簡単な変換:すべての文字列値を大文字に変換する
        if isinstance(data, dict):
            return {k: v.upper() if isinstance(v, str) else v
                   for k, v in data.items()}
        elif isinstance(data, list):
            return [item.upper() if isinstance(item, str) else item
                   for item in data]
        return data

class CSVProcessor(DataProcessor):
    """CSVデータプロセッサ"""
   
    def read_data(self, file_path):
        data = []
        with open(file_path, 'r', encoding='utf-8') as f:
            reader = csv.DictReader(f)
            for row in reader:
                data.append(row)
        return data
   
    def transform_data(self, data):
        # 各レコードに処理タイムスタンプを追加する
        import datetime
        timestamp = datetime.datetime.now().isoformat()
        for record in data:
            record['processed_at'] = timestamp
        return data
   
    def cleanup(self):
        """クリーンアップメソッドをオーバーライドし、追加のクリーンアップロジックを追加"""
        print("CSVプロセッサのクリーンアップ完了")
        print("CSVパーサーのリソースを解放する")

# 使用例
if __name__ == "__main__":
    # テストデータを作成する
    import os
   
    # JSONプロセッサをテストする
    json_data = {'name': 'john', 'age': 30, 'city': 'new york'}
    with open('test.json', 'w') as f:
        json.dump(json_data, f)
   
    json_processor = JSONProcessor()
    json_processor.process_data('test.json', 'output_json.txt')
   
    print("\n" + "="*50 + "\n")
   
    # CSVプロセッサをテストする
    csv_data = "name,age,city\njohn,30,new york\njane,25,los angeles"
    with open('test.csv', 'w') as f:
        f.write(csv_data)
   
    csv_processor = CSVProcessor()
    csv_processor.process_data('test.csv', 'output_csv.txt')
   
    # テストファイルをクリーンアップする
    for file in ['test.json', 'test.csv', 'output_json.txt', 'output_csv.txt']:
        if os.path.exists(file):
            os.remove(file)

例2:飲み物作成テンプレート

例

from abc import ABC, abstractmethod

class BeverageMaker(ABC):
    """飲み物作成テンプレート"""
   
    def make_beverage(self):
        """飲み物を作成するテンプレートメソッド"""
        self.boil_water()
        self.brew()
        self.pour_in_cup()
        if self.customer_wants_condiments():
            self.add_condiments()
        self.serve()
   
    def boil_water(self):
        """お湯を沸かす - 具体メソッド"""
        print("お湯を沸かす")
   
    @abstractmethod
    def brew(self):
        """抽出する - 抽象メソッド"""
        pass
   
    def pour_in_cup(self):
        """カップに注ぐ - 具体メソッド"""
        print("カップに注ぐ")
   
    @abstractmethod
    def add_condiments(self):
        """調味料を追加する - 抽象メソッド"""
        pass
   
    def customer_wants_condiments(self):
        """フックメソッド - お客様が調味料を必要とするかどうか"""
        return True
   
    def serve(self):
        """飲み物を提供する - 具体メソッド"""
        print("飲み物ができました。お召し上がりください!")

class CoffeeMaker(BeverageMaker):
    """コーヒー作成"""
   
    def brew(self):
        print("沸騰したお湯でコーヒー粉を抽出する")
   
    def add_condiments(self):
        print("砂糖と牛乳を加える")
   
    def customer_wants_condiments(self):
        answer = input("コーヒーに砂糖と牛乳を加えますか?(y/n): ")
        return answer.lower() == 'y'

class TeaMaker(BeverageMaker):
    """お茶作成"""
   
    def brew(self):
        print("沸騰したお湯で茶葉を浸す")
   
    def add_condiments(self):
        print("レモンを加える")

# 使用例
if __name__ == "__main__":
    print("コーヒーを作成:")
    coffee = CoffeeMaker()
    coffee.make_beverage()
   
    print("\n" + "="*30 + "\n")
   
    print("お茶を作成:")
    tea = TeaMaker()
    tea.make_beverage()

テンプレートメソッドパターンの変種

1. パラメータ付きテンプレートメソッド

例

class ConfigurableProcessor(ABC):
    """設定可能なデータプロセッサ"""
   
    def process_with_config(self, input_file, output_file, config):
        """設定パラメータ付きテンプレートメソッド"""
        self.validate_config(config)
        data = self.read_data(input_file)
        processed_data = self.process_with_config_impl(data, config)
        self.save_data(processed_data, output_file)
        self.post_process(config)
   
    def validate_config(self, config):
        """設定の検証 - 具体メソッド"""
        required_keys = ['format', 'encoding']
        for key in required_keys:
            if key not in config:
                raise ValueError(f"設定に必須のキーがありません: {key}")
   
    @abstractmethod
    def process_with_config_impl(self, data, config):
        """設定を使用してデータを処理する実装"""
        pass
   
    def post_process(self, config):
        """後処理 - フックメソッド"""
        if config.get('cleanup', False):
            print("クリーンアップ操作を実行")

2. マルチステップテンプレートメソッド

例

class MultiStepProcessor(ABC):
    """マルチステッププロセッサ"""
   
    def complex_processing(self):
        """複雑な処理フロー"""
        self.initialize()
        self.pre_process()
        self.main_process()
        self.post_process()
        self.finalize()
   
    def initialize(self):
        print("プロセッサを初期化する")
   
    def pre_process(self):
        """前処理 - フックメソッド"""
        pass
   
    @abstractmethod
    def main_process(self):
        """主要な処理ロジック"""
        pass
   
    def post_process(self):
        """後処理 - フックメソッド"""
        pass
   
    def finalize(self):
        print(処理が完了しました)

ベストプラクティスと注意事項

1. 抽象メソッドの適切な使用

  • 本当にサブクラスの実装が必要なメソッドのみを抽象メソッドとして宣言する
  • オプションのステップにはデフォルト実装のフックメソッドを提供する

2. テンプレートメソッドのアクセス制御

  • テンプレートメソッドは通常、次のように宣言すべきですfinal(Pythonでは命名規則によって実現できます)
  • ステップメソッドは外部から直接呼び出されないように保護されるべきです

3. エラーハンドリング

例

class RobustTemplate(ABC):
    """堅牢なテンプレートクラス"""
   
    def template_method(self):
        try:
            self.setup()
            self.execute_steps()
        except Exception as e:
            self.handle_error(e)
        finally:
            self.cleanup()
   
    def execute_steps(self):
        """実行ステップのシーケンス"""
        self.step1()
        self.step2()
        self.step3()
   
    def handle_error(self, error):
        """エラーハンドリング - フックメソッド"""
        print(f"処理中にエラーが発生しました: {error}")
   
    @abstractmethod
    def setup(self):
        pass
   
    @abstractmethod
    def step1(self):
        pass
   
    @abstractmethod
    def step2(self):
        pass
   
    @abstractmethod
    def step3(self):
        pass
   
    def cleanup(self):
        print("リソースのクリーンアップ")

実践演習

さあ、あなたの番です!以下の演習を完了して、テンプレートメソッドパターンの理解を深めましょう:

演習1:ファイルエクスポートテンプレートの実装

さまざまな形式(TXT、HTML、JSON)へのエクスポートをサポートするファイルエクスポートテンプレートを作成します。

要件:

  • 抽象基底クラスを定義するFileExporter
  • 具体的なサブクラスを実装するTXTExporter、HTMLExporter、JSONExporter
  • テンプレートメソッドには、データの準備、データのフォーマット、ファイルの保存、後処理などのステップを含める必要があります。

演習2:飲み物作成テンプレートの改良

以前の飲料作成テンプレートを拡張し、以下の機能を追加します:

  • 飲料のサイズ選択(小カップ、中カップ、大カップ)をサポートする
  • 価格計算機能を追加する
  • カスタム調味料をサポートする

まとめ

テンプレートメソッドパターンは、強力で実用的なデザインパターンであり、以下の方法でより良いコードを構築するのに役立ちます:

主な利点

  1. コードの再利用性の向上:共通コードを親クラスに配置する
  2. 拡張性の向上:サブクラスで特定のステップを拡張する
  3. 開放閉鎖の原則に準拠:拡張に対して開かれ、修正に対して閉じている
  4. 制御の反転:親クラスがフローを制御し、サブクラスが詳細を実装する

適用シナリオ

  • 複数のクラスに同じメソッドがあるが、具体的な実装が異なる
  • サブクラスの拡張ポイントを制御する必要がある
  • アルゴリズムの骨格を定義する必要があるが、一部のステップの変更を許可する

核心ポイント

  • テンプレートメソッドはアルゴリズムの不変部分を定義する
  • 抽象メソッドはアルゴリズムの可変部分を定義する
  • フックメソッドはオプションの拡張ポイントを提供する
その他の拡張