検索ガイド -Search Guide-

単語と単語を空白で区切ることで AND 検索になります。
例: python デコレータ ('python' と 'デコレータ' 両方を含む記事を検索します)
単語の前に '-' を付けることで NOT 検索になります。
例: python -デコレータ ('python' は含むが 'デコレータ' は含まない記事を検索します)
" (ダブルクオート) で語句を囲むことで 完全一致検索になります。
例: "python data" 実装 ('python data' と '実装' 両方を含む記事を検索します。'python data 実装' の検索とは異なります。)
当サイトのドメイン名は " getwebtips.net " です。
トップレベルドメインは .net であり、他の .com / .shop といったトップレベルドメインのサイトとは一切関係ありません。
practical_python_design_patterns

Practical Python Design Patterns - Python で学ぶデザインパターン: The Command Pattern - Part. 2 「コマンドパターンの実装」の巻 投稿一覧へ戻る

Published 2022年6月21日6:14 by T.Tsuyoshi

SUPPORT UKRAINE

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

Practical Python Design Patterns - Command Pattern 編
「コマンドパターンの実装」の巻

The Command Pattern
(コマンドパターンの実装)

あるオブジェクトから他のオブジェクトへ何かしらの命令や命令セットをオブジェクト間の結合は疎の状態を保ったまま送信したい場合は常に、命令を実行するために必要なすべてのものをある種のデータ構造としてカプセル化するのが賢明です。
実行を開始するクライアントは、命令が実行される方法について何1つ知っている必要はありません。為すべきことは、必要とされるすべての情報をセットアップし、システム内で次に実行されるべき「何か」に渡すことだけです。
このシリーズではオブジェクト指向に則って話を進めてきていますのから、命令 (セット) をカプセル化するためのデータ構造も必然的に「オブジェクト」になります。そして、他のあるメソッドが実行するために必要とする情報をカプセル化したオブジェクトのクラスを「コマンド (command)」と呼んでいます。
クライアントオブジェクトは、「実行したいメソッド」「そのメソッドが実行時に必要とするパラメータ」を含むコマンドをインスタンス化すると同時に、「『実行したいメソッド』から呼び出されて実行されるメソッドを有する他のオブジェクト (ターゲットオブジェクト)」もインスタンス化します。
この「ターゲットオブジェクト」は「レシーバー (receiver)」と呼ばれ、コマンドオブジェクトの実行時に「実行時に必要とするパラメータ」を受け取って実際の機能を果たすメソッドを含むクラスのインスタンスです。
そしてこれら全ては、レシーバーのメソッドの実行決定権を握る invoker (実施者・発動者) と呼ばれるオブジェクトの「支配下」に入っています。
実際の機能を何一つ含まないコマンドパターン (command pattern) の骨組みは次のようになります:
class Command:
def __init__(self, receiver, text):
self.receiver = receiver
self.text = text

def execute(self):
self.receiver.print_message(self.text)


class Receiver:
def print_message(self, text):
print(f'メッセージを受信しました: {text}')


class Invoker:
def __init__(self):
self.commands = []

def add_command(self, command):
self.commands.append(command)

def run(self):
for command in self.commands:
command.execute()


if __name__ == '__main__':
receiver = Receiver()

command1 = Command(receiver, 'Command 1 を実行します')
command2 = Command(receiver, 'Command 2 を実行します')

invoker = Invoker()
invoker.add_command(command1)
invoker.add_command(command2)
invoker.run()
この処理パターンによって、レシーバーが他のコマンドを実行中であったとしても、その後に実行すべきコマンドをキューに追加しておくことが可能となります。このコンセプトは、システムが他の処理でビジーであったとしても着信したコマンドを受け付ける必要がある分散システム (distributed system) において非常に有用です。このように、「実行すべきこと」をキューに入れておき可能になった時点で処理を行うことが可能になるのは、それぞれのオブジェクトが実行に必要な情報を全てカプセル化しているからです。これによって、他の処理でビジーであっても着信した重要なコマンドを失う心配はなく、後で確実に実行できます。
またこの構造からも分かるように、システムの実行時に動的に動作を組み立てることができます。この特徴を利用すれば、まずその時々で必要な実行すべきメソッドのセットアップを済ませておき、必要に応じて実行する、という操作を実現することが可能となります。
Mars Rover のような遠隔操作ロボットについて少し考えてみましょう。オペレーターが1度に送信可能なコマンドは1つだけであり、次のコマンドが送信可能になるのは前のコマンドの実行が終了してから、であったとしたらどうでしょう?火星上で為すべきあらゆる活動は時間がかかり過ぎて現実的ではなくなってしまうでしょう。その代わり、ローバーには「活動セット (sequence of actions)」を送信するようにします。ローバー側では「為すべき活動」を1つ1つ取り出して実行するようにすることで、単位時間内にこなせる仕事量は段違いに増加するはずです。それと同時に、ローバーと地球の間の送信ラグを解消することにも繋がります。このような「一連の活動セット」のことを「マクロ (macro)」と呼んでいます。
マクロを利用することで、システムに対するユーザーの操作を検証するテストの自動化や、繰り返し繰り返し実行する必要があるタスクの自動化などが可能となります。プロセスの一連の流れを構成するコマンドを作成し、1つ1つのコマンドを invoker (実行を司るハンドラ) に渡すのではなく、それらを一連の「活動セット == 命令セット」として invoker に渡すようにします。こうした「構成」と「実行」の分離によって、キーボート等を利用した人間の直接的な操作をコンピューターが作成する疑似的な入力に置き換えることが容易になります。
また、「コマンドオブジェクトの並び」としての命令セットの導入によって、「実行済み」コマンドの「やり直し (undo)」機能を実装することが可能となります。この分野もコマンドパターンの独断場です。実行が終了したコマンドオブジェクト自体を「やり直しスタック (undo stack)」に積んでおくことで、そのコマンドのやり直しを容易に実現できるだけでなく、スタックに積んである全ての「実行済み」コマンドの「何層にも渡る」やり直し機能も簡単に実現できてしまいます。この機能は、文書編集ソフトや動画編集ソフトといったほぼすべてのアプリケーションに搭載されているものです。
コマンドパターンを利用した複数レベルに渡る「やり直しスタック (undo stack)」を実装例から見てみましょう。ここでは和差積商を求める単純な電卓を作成します。コードをできる限り単純化するため、0による乗算・除算は考慮しません:
class AddCommand:
def __init__(self, receiver, value):
self.receiver = receiver
self.value = value

def execute(self):
self.receiver.add(self.value)

def undo(self):
self.receiver.subtract(self.value)


class SubtractCommand:
def __init__(self, receiver, value):
self.receiver = receiver
self.value = value

def execute(self):
self.receiver.subtract(self.value)

def undo(self):
self.receiver.add(self.value)


class MultiplyCommand:
def __init__(self, receiver, value):
self.receiver = receiver
self.value = value

def execute(self):
self.receiver.multiply(self.value)

def undo(self):
self.receiver.divide(self.value)


class DivideCommand:
def __init__(self, receiver, value):
self.receiver = receiver
self.value = value

def execute(self):
self.receiver.divide(self.value)

def undo(self):
self.receiver.multiply(self.value)


class CalculationInvokder:
def __init__(self):
self.commands = []
self.undo_stack = []

def add_new_command(self, command):
self.commands.append(command)

def run(self):
for command in self.commands:
command.execute()
self.undo_stack.append(command)

def undo(self):
undo_command = self.undo_stack.pop()
undo_command.undo()


class Accumulator:
def __init__(self, value):
self._value = value

def add(self, value):
self._value += value

def subtract(self, value):
self._value -= value

def multiply(self, value):
self._value *= value

def divide(self, value):
self._value /= value

def __str__(self):
return f'現在の値: {self._value}'


if __name__ == '__main__':
receiver = Accumulator(10.0)

invoker = CalculationInvokder()
invoker.add_new_command(AddCommand(receiver, 11))
invoker.add_new_command(SubtractCommand(receiver, 12))
invoker.add_new_command(MultiplyCommand(receiver, 13))
invoker.add_new_command(DivideCommand(receiver, 14))

invoker.run()
print(receiver)

invoker.undo()
print(receiver)

invoker.undo()
print(receiver)

invoker.undo()
print(receiver)

invoker.undo()
print(receiver)
このシリーズではすでに何回も見てきていますが、ダックタイピング言語である Python を利用している限り、各コマンドクラスが備えるべきインターフェースを定義したインターフェースクラスを別に定義する必要はありません。execute() メソッドと undo() メソッド、receiver と value を引数として取るコンストラクタさえ備えていれば、そのクラスは「command クラスに違いない」という推測のもと利用することができます。
この undo 機能実装例では、まず invoker オブジェクトの実行コマンドスタックに実行したい全てのコマンドをセットしています。invoker における各コマンドの実行時には、各コマンドの execure() メソッドが呼び出されて実際の計算処理が行われ、その処理の終了を待ってその「実行済み」コマンドを undo 用コマンドスタックに積んでいます。そして invoker に undo の命令が届くと、一番最後に実行されたコマンドが undo 用コマンドスタックから取り出され (pop) そのコマンドの undo() メソッドが実行されることになっています。
この計算機の例における undo 操作では、リバース計算を実行することによってレシーバー (Accumulator オブジェクト) に保存されている計算値をそのコマンド自身が実行される以前の値に戻しています。
コマンドパターンにおける command クラスの実装では、操作を実行する段階で invoker から呼び出される execute() を定義します。また command クラスのインスタンスでは、「実際の操作」を行う レシーバークラスオブジェクト/レシーバー関数 への参照と、その実行時に必要なパラメータを保持しています。
コマンドパターンにおいて「実際の処理の内容」を把握しているのはレシーバーオブジェクトだけです。そして、処理の結果を保持する内部変数を管理しています。当然、この内部変数はシステム内の他のすべてのオブジェクトから独立しています。
クライアントはコマンドの実装詳細について何1つ知りませんし invoker に対しても同様です。また、お気付きでしょうが、コマンドオブジェクト自身も何か具体的な処理を行っているわけではなく、ただのコンテナ (container) に過ぎません。
コマンドパターンを採用することによって、「実際に行われる処理」と「その処理を開始させるオブジェクト」を分離する抽象化レイヤー (abstraction layer) を形成することになります。そして追加されたこの抽象化レイヤーによって、システム内の様々なオブジェクト間の相互作用が向上する一方、結合は「疎」となるため、システム全体の保守と更新が容易になります。
このデザインパターンの要点は、「メソッドの呼び出し」を変数として保存し、パラメータとしてメソッドや関数に渡し、結果としてメソッドや関数から返される「データ」と見なしている、ということです。逆に言えば、関数やメソッドに「第一級市民 (first-class citizens)」としての特徴を付与するのがこの command pattern の適用成果だ、ということになります。繰り返しになりますが、関数やメソッドが first-class citizens である、ということは、関数を変数として扱える、ということであり、関数を他の関数の引数として渡すことができる、ということであり、ある関数の実行結果として関数を返すことができる、ということです。
コマンドパターン、特に「振る舞い」に関わる部分を第一級市民として扱うという機能に関して興味深いことは、このパターンを採用することで遅延実行 (lazy evaluation) を伴う関数型プログラミングスタイル (functional programming style) を実装することが可能となることでしょう。つまり、全ての関数は実行時に必要な全てのデータ (ただしグローバル値は含みません) とともに渡され、実際の結果が必要とされる時期が来たときのみ実行されます。関数型プログラミングについての詳細は省かざる負えませんが (これだけで十分一冊の本になりますから...)、これが示唆していることは非常に深淵です。
しかしこの話にはちょっとしたオチがありまして、Python を利用する我々にはここまで述べてきたコマンドパターンの特徴・効用といったものはそこまで特別なものではありません。それは、Python における関数はすでに first-class citizen であるため、ここまでのコード例でみてきたような、実行関数の呼び出しとその実行時に引数として利用するデータをカプセル化するためのクラス (コマンドクラス) を必ずしも必要とはしないのです。その代わり invoker に対して、実行関数自体と実行時に必要なパラメータをセットとした「コマンドセット」を直接渡してしまうこともできるのです:
def text_writer(string1, string2):
print(f'文字列の出力 {string1} - {string2}')


class Invoker:
def __init__(self):
self.commands = []

def add_command(self, command):
self.commands.append(command)

def run(self):
for command in self.commands:
command['function'](*command['params'])


if __name__ == '__main__':
invoker = Invoker()

invoker.add_command({
'function': text_writer,
'params': ('Command 1', 'String 1'),
})
invoker.add_command({
'function': text_writer,
'params': ('Command 2', 'String 2'),
})
invoker.run()
そして更に、このような単純なケースでは実行関数自体の定義さえ必要ありません; lambda 式で間に合ってしまいます:
class Invoker:
def __init__(self):
self.commands = []

def add_command(self, command):
self.commands.append(command)

def run(self):
for command in self.commands:
command['function'](*command['params'])


if __name__ == '__main__':
f = lambda string1, string2: print(f'文字列の出力 {string1} - {string2}')

invoker = Invoker()

invoker.add_command({
'function': f,
'params': ('Command 1', 'String 1'),
})
invoker.add_command({
'function': f,
'params': ('Command 2', 'String 2'),
})
invoker.run()
しかし実行したい操作がより複雑なものである場合、実装方法としてのオプションはあと2つほど考えられます。1つ目は、あくまでも「コマンドクラスは定義しない」少し「こ洒落た」方法で、本来コマンドクラスで定義する execute() を lambda 式でラップしてしまって実行する、というものです。すなわち、lambda 式ではレシーバー関数の呼び出し、レシーバー関数へのパラメーターの引き渡しに専念する、わけです。2つ目は、実行すべき操作がより複雑な場合にコードをより「クリーン」に維持するための方法で、従来通り「ちゃんとコマンドクラスを定義する」というものです。
コマンドパターンを採用する際にどのような実装方法をとるか、の判断基準としては、1行のラムダ式で記述可能な操作よりも複雑な処理を実行する場合は「素直に」コマンドクラスを定義しよう、ということになるでしょうか。

Parting Shots
(ダメ押し確認)

自動運転車や宇宙船への指示送信時に実行要求 (命令セット) と実際の実行を切り離すことで、複数の命令をバッファリングすることが可能になるだけではなく、入力方法の変更さえも可能になります。
コマンドパターンにおける要点は、invoker と receiver の分離、ということです。このことが最終的に、命令セットの構築と実際の処理の実行の分離、に繋がります。

この投稿をメールでシェアする

0 comments

コメントはまだありません。

コメントを追加する(不適切と思われるコメントは削除する場合があります)