見出し画像

DP.15_1:コマンド(操作・関数)の生成と実行を分離する - Commandパターン1 -【Python】

【1】Commandパターン

Commandパターンは

Commandオブジェクトを作成して、その中の「execute()メソッド」をコールして、実際に起動させる関数・処理を呼び出す

というもの。

直接関数や処理を呼び出さない」ことにより
『Commandオブジェクトを用意するが実行しないでおく。あとでまとめて必要な処理を呼び出す』
ということがしやすくなる。

【2】実際の利用例

Commandパターンでは「コマンド(操作・関数)の生成や定義」と「実行」を分離される。このパターンは色々な場面で使用されている。

利用例1 [Qt(GUIアプリ)のQActionクラスオブジェクト]
ファイルを開く」という操作をするときに、「画面内に設置したボタンから開く」、「メニューバーの項目から開く」、「キーボードのショートカットキーから開く」、、、など手段は色々あるが、同じ操作なら同じコマンドをコールすればよくなる

利用例2 [操作・コマンドのマクロ記録]
例えばExcelのマクロRPA系ツールでは各種操作を記録する。その後、記録したコマンドをまとめて実行できる。

利用例3 [履歴保存やUnDo操作]
・システムログを使ったシステムクラッシュしたOSの「復旧」
・RDBのトランザクションと「ロギング」
・インストーラー(ウィザード)とキャンセル時の「操作巻き戻し」
・テキスト操作のコピペ、UnDo操作
・・・などなど

【3】例題:テキストファイルの操作

例題として「テキストファイルの作成(Create)・読み込み(Read)・リネーム(Rename)の3つ」を「Commandパターン」で実装してみる。

【作成するプログラム】

「Create→Read→Rename」という順序でコマンドをグループ化して、この順番で実行する


※なお、Commandパターンの実現の仕方には、

(a) 全ての処理をCommandオブジェクトに含める書き方
(b) Receiver、Invokerオブジェクトを用意してさらに分離する書き方

がある。
説明を簡単にするためにここでは「(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]

## 実行結果
[creating file 'file1']
[reading file 'file1']
hello world
[renaming 'file1' to 'file2']
# → file2というファイルができあがっている。

▲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()

# 実行結果
[creating file 'file1']
[reading file 'file1']
hello world
[renaming 'file1' to 'file2']
(undo)[renaming 'file2' to 'file1']
finished

【補足】第一級オブジェクト(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パターンの書き方は次回にまとめる予定。





もっと応援したいなと思っていただけた場合、よろしければサポートをおねがいします。いただいたサポートは活動費に使わせていただきます。