検索ガイド -Search Guide-

単語と単語を空白で区切ることで AND 検索になります。
例: python デコレータ ('python' と 'デコレータ' 両方を含む記事を検索します)
単語の前に '-' を付けることで NOT 検索になります。
例: python -デコレータ ('python' は含むが 'デコレータ' は含まない記事を検索します)
" (ダブルクオート) で語句を囲むことで 完全一致検索になります。
例: "python data" 実装 ('python data' と '実装' 両方を含む記事を検索します。'python data 実装' の検索とは異なります。)
practical_python_design_patterns

Practical Python Design Patterns - Python で学ぶデザインパターン: The Template Method Pattern「第17章: テンプレートメソッドパターン」の巻 投稿一覧へ戻る

Published 2022年7月6日4:09 by mootaro23

SUPPORT UKRAINE

- Your indifference to the act of cruelty can thrive rogue nations like Russia -

Chapter 17: Template Method Pattern
(第17章: テンプレートメソッドパターン)

Success without duplication is merely future failure in disguise.
(パターンなき成功は将来における定められた失敗の見せかけの姿)
- Randy Gage in How to build a multi-level money machine; the science of network marketing
Overview
(概要 - テンプレートメソッドパターン編)
日常生活においてもコーディングにおいても、1ステップ1ステップを積み上げていけば目標に到達できるような「決まったパターン」なり「ちょっとした複数のアクションの手順」といったものが存在します。状況が複雑な場合はそれらを構成する1ステップ毎の内容は異なるかもしれませんが、それでも全体の構成が大きく変わることはありません。
生活面では、そういった標準的な行動の手順を書き留めておいたり、そういったものから経験則 (rules of thumb) やら自分なりのメンタルモデルといったものを導き出す人もいるかもしれません。コーディングの面では、それはすなわち「コードの再利用」を意味します。
「コードの再利用」はオブジェクト指向プログラミンがこれだけ広く普及した主な理由の1つです。オブジェクト指向プログラミンが目指す最終的な目標は、ある1つのプロジェクトにおける DRY 原則の順守ではなく複数のプロジェクトに渡って DRY 原則に従うことと、キャリアを積み重ねる中でのツール類の構築です。関わる全てのプロジェクトがあなたをプログラマーとして成長させるだけではなく、「武器」となるツール類の蓄積によるあなたの「武器庫」の巨大化に貢献してくれるはずです。
中には、このような「デザインパターン」を扱う本・記事の存在自体が DRY 原則に違反している、と主張する人もいます。それは、ある問題をコード化し解決するための各種パターンの存在は、その問題を再度解決しなければならない必要性を排除するものだからだ、というものです。
しかし、数年以上の経験を持つプログラマーであれば誰でも証言できるように、「完全なコードの再利用」はまだ実現できていません。新たなプログラミング言語が次から次へと産み出されその数の増加が留まることを知らないことも、これが「決着した」問題ではないことを裏付けています。
皆さんの「やる気」を削ぐつもりはありません。しかし、開発者として認識しておくべき必要があることは、我々は常に理想にとらわれ過ぎず現実的でなければならない、ということです。そして、「世の中はこうあるべきである」という理想とはマッチしない決定を下さなければならないことが多々ある、ということです。そのような決定を下さなければならないことにイラ立ち、より良い解決策を追い求めることは問題ではありません; しかしそれは、そういった姿勢が「現実の仕事」に持ち込まれなければ、という前提でです。
しかし、ストレトジーパターンなどのデザインパターンを利用することで、プログラム全体を変更することなく改良を施すことが可能、という「理想」を追い求めることができることも事実ではあります。このシリーズを通してお話ししてきている「疎な結合」「関心の分離」も物事が変化したときに対処すべき仕事量を減少させるための1つの手段です - そして物事は常に変化し続けます。
独自のニュアンスを有する様々なコンテキストにおいて実行されるべきアクションの「確かな」パターンを識別する必要がある場合、我々な何をなすべきでしょうか?
1つの解決策は「関数 (functions)」です。n の階乗 (factorial) を求めることを考えてみましょう。n の階乗、というのは n から 1 までの 全ての整数の積、です (ただし、0 および 1 の階乗は 1 です)。ですから n = 5 である場合は次のように表されます:
5! = 5 * 4 * 3 * 2 * 1
常に n = 5 の階乗を求めるケースだけを考えればいいのであればこれで十分です。しかしより一般的な解決策があった方が何かと役に立ちますよね:
def fact(n):
if n < 2:
return 1

return n * fact(n - 1)

def main():
print(fact(0))
print(fact(1))
print(fact(5))

if __name__ == '__main__':
main()
これは、ある特定の手順を踏むことで期待通りの結果が得られる、という例です。関数はこのような、ある入力 or 入力セットに対して期待される結果を返すアルゴリズムや一連のステップを実装する優れた手段です。しかし、解決すべき問題が事前に用意しておいた「指示書」に従うより複雑になってきた場合はそれに伴い実装も困難さを増してきます。またもう1つ考慮すべき点は、同じ一連の手順を異なる方法で、もしくは、より効率的なアルゴリズムを使って実装したい場合にはどうすべきなのか、ということです。この問題を考えてみるには階乗を求める問題では少し単純すぎますから、少しだけ複雑な例を取り上げましょう。
あなたが作成した POS システムが突如人気を博し、顧客から様々な要望が寄せられるようになりました。それらの要望の主たるものは、彼らが使用し続けてきている在庫と価格を追跡するためのサードパーティー製のシステムを POS システムと連携してほしい、というものです。顧客側では現在のシステムを入れ替える意思はないため、あなたとしては何とかこの2つのシステムを統合するしかありません。
さあ、ちょっと落ち着いて、この外部システムとの統合に必要なステップを考えてみましょう:
POS システムと外部システムの間で在庫を同期させる
外部システムに対して販売データを送信する
これは必要なステップを非常に単純化したものですが十分に役に立ちます。以前の例のように単純な関数で表現するとすれば、プロセスにおける各ステップをそれぞれ別の関数として定義、実装し、適切な時点でそれらを呼び出すことになるでしょう:
def sync_stock_items():
print('ローカルシステムと外部システム間の在庫同期処理を実行します')
print('外部システムの在庫情報取得')
print('ローカルシステムでの在庫情報更新')
print('更新済み在庫情報を外部システムへ送信')

def send_transaction(transaction):
print(f'取引情報送信: {repr(transaction)}')

def main():
sync_stock_items()

send_transaction(
{
'id': 1,
'items': [
{
'item_id': 1,
'amount_purchased': 3,
'value': 238,
},
],
}
)

if __name__ == '__main__':
main()
実行結果:
ローカルシステムと外部システム間の在庫同期処理を実行します
外部システムの在庫情報取得
ローカルシステムでの在庫情報更新
更新済み在庫情報を外部システムへ送信
取引情報送信: {'id': 1, 'items': [{'item_id': 1, 'amount_purchased': 3, 'value': 238}]}
次にやるべきことは、現実世界で使用する場合のプログラムの評価です。現時点で統合を考慮すべき外部システムは1つだけですが、もしほかに2つあったとしたらどうでしょう?
あまり深く考えずに対処するとすれば、それぞれのシステムに対して処理を分けるための if 文を書き連ねることになるでしょう。次に例を示しますが、問題の部分のみを書き出しています:
def sync_stock_items(system):
if system == 'system1':
print('ローカルシステムと外部システム 1 間の在庫同期処理を実行します')
print('外部システム 1 の在庫情報取得')
print('ローカルシステムでの在庫情報更新')
print('更新済み在庫情報を外部システム 1 へ送信')
elif system == 'system2':
print('ローカルシステムと外部システム 2 間の在庫同期処理を実行します')
print('外部システム 2 の在庫情報取得')
print('ローカルシステムでの在庫情報更新')
print('更新済み在庫情報を外部システム 2 へ送信')
elif system == 'system3':
print('ローカルシステムと外部システム 3 間の在庫同期処理を実行します')
print('外部システム 3 の在庫情報取得')
print('ローカルシステムでの在庫情報更新')
print('更新済み在庫情報を外部システム 3 へ送信')
else:
print('該当するシステムは存在しません')

def send_transaction(transaction, system):
if system == 'system1':
print(f'外部システム 1 へ取引情報送信: {repr(transaction)}')
elif system == 'system2':
print(f'外部システム 2 へ取引情報送信: {repr(transaction)}')
elif system == 'system3':
print(f'外部システム 3 へ取引情報送信: {repr(transaction)}')
else:
print('該当するシステムは存在しません')
必要な処理を実行するための引数だけではなく、連携を取るべきシステムを区別するための情報も常に渡さなければなりません。またこのシリーズにおいて何回も議論してきているように、こういった方法によるそれなりの規模のシステムの実装は長い目で見たときに「悲劇」でしかありません。ではどうしたらいいでしょう?このシリーズは「デザインパターン」を取り上げていますから、問題の解決策としても勿論デザインパターンを利用し、メンテナンス、更新、拡張が容易な方法を考えたいわけです。やりたいことははっきりしています。既存のコードに変更を加えることなく新たな外部システムとの連携を組み込めるようにしたいのです。前回取り上げたストレトジーパターンを使ってそれぞれの外部システムに対する「戦略」を実装してみましょう:
def sync_stock_items(strategy_func):
strategy_func()

def send_transaction(transaction, strategy_func):
strategy_func(transaction)

def stock_sync_strategy_system1():
print('ローカルシステムと外部システム 1 間の在庫同期処理を実行します')
print('外部システム 1 の在庫情報取得')
print('ローカルシステムでの在庫情報更新')
print('更新済み在庫情報を外部システム 1 へ送信')

def stock_sync_strategy_system2():
print('ローカルシステムと外部システム 2 間の在庫同期処理を実行します')
print('外部システム 2 の在庫情報取得')
print('ローカルシステムでの在庫情報更新')
print('更新済み在庫情報を外部システム 2 へ送信')

def stock_sync_strategy_system3():
print('ローカルシステムと外部システム 3 間の在庫同期処理を実行します')
print('外部システム 3 の在庫情報取得')
print('ローカルシステムでの在庫情報更新')
print('更新済み在庫情報を外部システム 3 へ送信')

def send_transaction_strategy_system1(transaction):
print(f'外部システム 1 へ取引情報送信: {repr(transaction)}')

def send_transaction_strategy_system2(transaction):
print(f'外部システム 2 へ取引情報送信: {repr(transaction)}')

def send_transaction_strategy_system3(transaction):
print(f'外部システム 3 へ取引情報送信: {repr(transaction)}')

def main():
transaction = {
'id': 1,
'items': [
{
'item_id': 1,
'amount_purchased': 3,
'value': 238,
},
],
}

print('=' * 10)
sync_stock_items(stock_sync_strategy_system1)
send_transaction(
transaction,
send_transaction_strategy_system1
)
print('=' * 10)
sync_stock_items(stock_sync_strategy_system2)
send_transaction(
transaction,
send_transaction_strategy_system2
)
print('=' * 10)
sync_stock_items(stock_sync_strategy_system3)
send_transaction(
transaction,
send_transaction_strategy_system3
)

if __name__ == '__main__':
main()
実行結果は if 文が氾濫していた前のコードスニペット例と変わりません:
==========
ローカルシステムと外部システム 1 間の在庫同期処理を実行します
外部システム 1 の在庫情報取得
ローカルシステムでの在庫情報更新
更新済み在庫情報を外部システム 1 へ送信
外部システム 1 へ取引情報送信: {'id': 1, 'items': [{'item_id': 1, 'amount_purchased': 3, 'value': 238}]}
==========
ローカルシステムと外部システム 2 間の在庫同期処理を実行します
外部システム 2 の在庫情報取得
ローカルシステムでの在庫情報更新
更新済み在庫情報を外部システム 2 へ送信
外部システム 2 へ取引情報送信: {'id': 1, 'items': [{'item_id': 1, 'amount_purchased': 3, 'value': 238}]}
==========
ローカルシステムと外部システム 3 間の在庫同期処理を実行します
外部システム 3 の在庫情報取得
ローカルシステムでの在庫情報更新
更新済み在庫情報を外部システム 3 へ送信
外部システム 3 へ取引情報送信: {'id': 1, 'items': [{'item_id': 1, 'amount_purchased': 3, 'value': 238}]}
この実装には気にくわないところが2カ所ほどあります。1つは、それぞれの「戦略」の内容は全く同じであるにも関わらず「別関数」として実装している、ということです。これは単一のエンティティとして実装する必要があります。もう1つは実行時における「同じような作業」の繰り返しです。ターゲットとする外部システム毎に「戦略」を分けた結果、それらの戦略を対象とする外部システムを指定しながら繰り返し繰り返し呼び出す必要が生じてしまっています。これもある種の DRY 原則の違反と見なせます。結論として、このシステムとストレトジーパターンの相性は良くない、と言えるでしょう。他の方法を考える必要があります。
Template method pattern (テンプレートメソッドパターン) は正にその名の通り、あるプロセスを1ステップ1ステップ処理し完了するために必要なメソッド群の「型紙」を提供するものです。またその「型紙」の処理操作の詳細を変更することで、異なる多くのシナリオで利用することが可能です。
テンプレートメソッドパターンの骨組みは一般的には次のような形になります:
import abc


class TemplateAbstractBaseClass(metaclass=abc.ABCMeta):

def template_method(self):
self._step_1()
self._step_2()
...
self._step_n()

@abc.abstractmethod
def _step_1(self):
pass

@abc.abstractmethod
def _step_2(self):
pass

...

@abc.abstractmethod
def _step_n(self):
pass


class ConcreteImplementationClass(TemplateAbstractBaseClass):

def _step_1(self):
pass

def _step_2(self):
pass

...

def _step_n(self):
pass
このパターンでは初めて Python の Abstract Base Classes (abc) ライブラリをその「真」の目的のまま使用しています。このシリーズではこれまで、他のあまり動的ではない言語で多用されるこの抽象ベースクラスをタイプヒントをつけるための「補佐的」なもの、としてしか扱ってきませんでした。それは Python がダックタイピング言語であるため、わざわざ抽象クラスを定義する必要がなかったためです。しかしテンプレートメソッドパターンに関しては状況が異なります。プロセスの各ステップを象徴するそれぞれのメソッドは抽象クラス内で抽象メソッドとして定義され、このクラスを継承する全てのサブクラスは、各ステップの実際の処理内容を自分なりに実装することを「強制」されます。しかし外部からはその実装詳細は見えず、あくまでも同じインターフェースを有する「プロセス処理クラス」でしかありません。
さぁ、テンプレートメソッドパターンを利用して我々の「外部システム統合プログラム」を実装し直してみましょう:
import abc


class ThirdPartyInteractionTemplate(metaclass=abc.ABCMeta):

def sync_stock_items(self):
self._sync_stock_items_step_1()
self._sync_stock_items_step_2()
self._sync_stock_items_step_3()
self._sync_stock_items_step_4()

def send_transaction(self, transaction):
self._send_transaction(transaction)

@abc.abstractmethod
def _sync_stock_items_step_1(self):
pass

@abc.abstractmethod
def _sync_stock_items_step_2(self):
pass

@abc.abstractmethod
def _sync_stock_items_step_3(self):
pass

@abc.abstractmethod
def _sync_stock_items_step_4(self):
pass

@abc.abstractmethod
def _send_transaction(self, transaction):
pass


class System1(ThirdPartyInteractionTemplate):
def _sync_stock_items_step_1(self):
print('ローカルシステムと外部システム 1 間の在庫同期処理を実行します')

def _sync_stock_items_step_2(self):
print('外部システム 1 の在庫情報取得')

def _sync_stock_items_step_3(self):
print('ローカルシステムでの在庫情報更新')

def _sync_stock_items_step_4(self):
print('更新済み在庫情報を外部システム 1 へ送信')

def _send_transaction(self, transaction):
print(f'外部システム 1 へ取引情報送信: {repr(transaction)}')


class System2(ThirdPartyInteractionTemplate):
def _sync_stock_items_step_1(self):
print('ローカルシステムと外部システム 2 間の在庫同期処理を実行します')

def _sync_stock_items_step_2(self):
print('外部システム 2 の在庫情報取得')

def _sync_stock_items_step_3(self):
print('ローカルシステムでの在庫情報更新')

def _sync_stock_items_step_4(self):
print('更新済み在庫情報を外部システム 2 へ送信')

def _send_transaction(self, transaction):
print(f'外部システム 2 へ取引情報送信: {repr(transaction)}')


class System3(ThirdPartyInteractionTemplate):
def _sync_stock_items_step_1(self):
print('ローカルシステムと外部システム 3 間の在庫同期処理を実行します')

def _sync_stock_items_step_2(self):
print('外部システム 3 の在庫情報取得')

def _sync_stock_items_step_3(self):
print('ローカルシステムでの在庫情報更新')

def _sync_stock_items_step_4(self):
print('更新済み在庫情報を外部システム 3 へ送信')

def _send_transaction(self, transaction):
print(f'外部システム 3 へ取引情報送信: {repr(transaction)}')

def main():
transaction = {
'id': 1,
'items': [
{
'item_id': 1,
'amount_purchased': 3,
'value': 238,
},
],
}

for C in [System1, System2, System3]:
print('=' * 10)
system = C()
system.sync_stock_items()
system.send_transaction(transaction)

if __name__ == '__main__':
main()
実行結果に変化はありません。しかし、テンプレートクラスによってこのプロセスを処理するために必要なステップが示され、またこれらのステップを順次実行する共通インターフェースが提供されたことで、main() 内の実行コードが非常にすっきりとしたことが分かると思います。

Parting Shots
(ダメ押し確認 - テンプレートメソッドパターン編)

何処でどのデザインパターンを使用し、この本や他の本で紹介しているパターンに依存すべきではないのは何処なのか、という判断は一種の「直感」です。武道と同様機会があれば常に練習する、つまり、パターンを使用できそうな機会があれば実装してみる、というのはそのような「直感」を養う上で非常に役に立ちます。そして何が機能し何が機能しなかったのかを考えることです。パターンが助けとなったのはどの部分なのか、逆に妨げとなったのはどのような時なのかを考えることです。そして最終的に、ある特定のデザインパターンで解決できる問題の種類と、パターンが逆に妨げとなってしまう問題の種類を見分ける「感覚」を養うことです。
あと前にも述べたことですが、毎日使用するツールを熟知することが必要です。お気に入りのエディタのドキュメントを見て、繰り返し記述しているボイラープレートコードを代わりに入力してくれるちょっとした関数が用意されていないかどうかを確認してみることをお勧めします。これらには、新しいファイルにコーディングを始める際の定型文を作成してくれるものや、main() 周りの構文を入力してくれるものなどが含まれるでしょう。
そういったコードスニペット入力機能に精通したり同様のツール類を自作したりすることで、自分のお気に入りのツール類の使い勝手が向上し、無駄なキーストロークに費やす時間をプログラムのアイデアを醸成させる時間に当てることができるようになります。