DP.15_1:コマンド(操作・関数)の生成と実行を分離する - Commandパターン1 -【Python】
【1】Commandパターン
Commandパターンは
というもの。
「直接関数や処理を呼び出さない」ことにより
『Commandオブジェクトを用意するが実行しないでおく。あとでまとめて必要な処理を呼び出す』
ということがしやすくなる。
【2】実際の利用例
Commandパターンでは「コマンド(操作・関数)の生成や定義」と「実行」を分離される。このパターンは色々な場面で使用されている。
【3】例題:テキストファイルの操作
例題として「テキストファイルの作成(Create)・読み込み(Read)・リネーム(Rename)の3つ」を「Commandパターン」で実装してみる。
【作成するプログラム】
※なお、Commandパターンの実現の仕方には、
がある。
説明を簡単にするためにここでは「(a):すべての処理をCommandオブジェクトん含める書き方」で話を進めていく。(※(b)の書き方はやや複雑なので後で)
■CreateFile コマンドオブジェクトの作成
必要となるのは実際に実行したい処理を呼び出すexecuteメソッド。
from dataclasses import dataclass
# @dataclassアノテーション(python3.7以降)
@dataclass
class CreateFile:
# __init__()は@dataclassで自動生成
path:str
txt:str = "hello world\n"
def execute(self):
print(f"[creating file '{self.path}']")
with open(self.path, mode='w', encoding='utf-8') as out_file:
out_file.write(self.txt)
▲しれっと使ったが「@dataclass」で「__init__()」の作成を手抜きしている。詳細は以下参照。
ようするに__init__()を自動生成してもらったということ。
※@dataclassを使わない場合
class CreateFile:
def __init__(self, path, txt='hello world\n' ):
self.path = path
self.txt = txt
...(略)...
※作成したコマンドオブジェクトの使い方
input_file = "file1"
create_file_cmd = CreateFile(input_file) # コマンドオブジェクト生成
create_file_cmd.execute() #コマンドオブジェクトから実行する
# file1 というファイルができあがる(中身はデフォルトhello worldという文章が入ってる)
▲こんな感じでコマンドオブジェクトにすることで、使用するコマンドの定義・生成と実行を分離できるようにする。
■ReadFile、RenameFile コマンドオブジェクトの作成
同様に残りのコマンドオブジェクトも作成する。
import os
@dataclass
class ReadFile:
path:str
def execute(self):
print(f"[reading file '{self.path}']")
with open(self.path, mode ='r', encoding='utf-8') as in_file:
print(in_file.read(), end='')
@dataclass
class RenameFile:
src : str
dst : str
def execute(self):
print(f"[renaming '{self.src}' to '{self.dst}']")
os.rename(self.src, self.dst)
■動作確認
作成したコマンドオブジェクトを使って、「Create→Read→Rename」を実行する一連のコマンドとして設定して起動してみる
input_file = "file1"
rename_file = "file2"
# Create → Read → Renameを一連のコマンドとしてセット
commands = (
CreateFile(input_file),
ReadFile(input_file),
RenameFile(input_file,rename_file)
)
# 順次実行する
[c.execute() for c in commands]
▲Commandパターンによって「コマンドをグループ化、特定の順序で実行する」、という動作が実現できた。
【4】全体コード
import os
from dataclasses import dataclass
# @dataclassアノテーション(python3.7以降)
@dataclass
class CreateFile:
# __init__()は@dataclassで自動生成
path:str
txt:str = "hello world\n"
# 実際の処理をコールするメソッド
def execute(self):
print(f"[creating file '{self.path}']")
with open(self.path, mode='w', encoding='utf-8') as out_file:
out_file.write(self.txt)
@dataclass
class ReadFile:
path:str
def execute(self):
print(f"[reading file '{self.path}']")
with open(self.path, mode ='r', encoding='utf-8') as in_file:
print(in_file.read(), end='')
@dataclass
class RenameFile:
src : str
dst : str
def execute(self):
print(f"[renaming '{self.src}' to '{self.dst}']")
os.rename(self.src, self.dst)
def main():
input_file = "file1"
rename_file = "file2"
# Create → Read → Renameを一連のコマンドとしてセット
commands = (
CreateFile(input_file),
ReadFile(input_file),
RenameFile(input_file,rename_file)
)
# 順次実行する
[c.execute() for c in commands]
if __name__ == "__main__":
main()
【5】おまけ:revered()を使ったちょっとしたUnDo操作の実現
pythonには組み込み関数として「reversed()」がある。
これを使って例題でグループ化したコマンドを逆順に実行するUndo相当のことを実現してみる。
(※本来は「ファイルの存在有無」をはじめ、実行時の状態に応じた様々なエラーチェック、例外処理などが必要だが省略)
■プログラムイメージ
... (略) ...
# Create → Read → Renameを一連のコマンドとしてセット
commands = (
CreateFile(input_file),
ReadFile(input_file),
RenameFile(input_file,rename_file)
)
... (略) ...
# 積んだコマンドを逆順に実行してUNDO相当のことをする
for c in reversed(commands):
try:
c.undo()
except AttributeError as e:
print("Error",str(e))
つまり、
それぞれのコマンドオブジェクトについて「undo()」を実装しておく
グループ化したコマンドオブジェクトを逆順に「undo()」コールする
というもの。
■全体コード
import os
from dataclasses import dataclass
# @dataclassアノテーション(python3.7以降)
@dataclass
class CreateFile:
# __init__()は@dataclassで自動生成
path:str
txt:str = "hello world\n"
# 実際の処理をコールするメソッド
def execute(self):
print(f"[creating file '{self.path}']")
with open(self.path, mode='w', encoding='utf-8') as out_file:
out_file.write(self.txt)
def undo(self):
os.remove(self.path)
@dataclass
class ReadFile:
path:str
def execute(self):
print(f"[reading file '{self.path}']")
with open(self.path, mode ='r', encoding='utf-8') as in_file:
print(in_file.read(), end='')
def undo(self):
pass
@dataclass
class RenameFile:
src : str
dst : str
def execute(self):
print(f"[renaming '{self.src}' to '{self.dst}']")
os.rename(self.src, self.dst)
def undo(self):
print(f"(undo)[renaming '{self.dst}' to '{self.src}']")
os.rename(self.dst, self.src)
def main():
input_file = "file1"
rename_file = "file2"
# Create → Read → Renameを一連のコマンドとしてセット
commands = (
CreateFile(input_file),
ReadFile(input_file),
RenameFile(input_file,rename_file)
)
# 順次実行する
[c.execute() for c in commands]
# 積んだコマンドを逆順に実行してUNDO相当のことをする
for c in reversed(commands):
try:
c.undo()
except AttributeError as e:
print("Error",str(e))
print("finished")
if __name__ == "__main__":
main()
【補足】第一級オブジェクト(first-class object)
今回の例や第3回のbuilderパターンでも出現していた以下のような書き方について。
※今回の例(クラスオブジェクトをグループ化するため積んでいる)
# Create → Read → Renameを一連のコマンドとしてセット
commands = (
CreateFile(input_file),
ReadFile(input_file),
RenameFile(input_file,rename_file)
)
# 順次実行する
[c.execute() for c in commands]
※第3回 builder パターンのdirectorオブジェクト(全体コード1より)
self.builder = ComputerBuilder() #使用するbuilderを指定
steps = (self.builder.configure_memory(memory),
self.builder.configure_ssd(ssd),
self.builder.configure_gpu(gpu))
[step for step in steps]
※第3回 builder パターンのdirectorオブジェクト(全体コード2より)
self.builder = builder
# stepsとして実行する関数オブジェクトをつんでおく
steps = (builder.prepare_dough,
builder.add_sauce,
builder.add_topping,
builder.bake)
# 指定した関数オブジェクトを順番に実行する
[step() for step in steps]
▲builderパターンの方は関数をグループ化して積んでいる
pythonでは関数もオブジェクトであるため、グループとしてまとめることができる。→ コマンドオブジェクトのような使い方ができる。
それらしい言葉で言うと
Pythonの「関数」は第一級オブジェクト(first-class object)である
という話。
「第一級オブジェクト(first-class object)」というのは
ということ。
もう少しざっくりいうとpythonでは
・関数に変数を割り当てる
・引数に別の関数を与える
みたいなことができるということ。
…長くなったので、ReceiverやInvokerを使ったCommandパターンの書き方は次回にまとめる予定。