Commandパターン

概要

個々の処理を1つのコマンドとしてまとめる」パターンです。
これにより、個々の処理の再利用化がやりやすくなります

処理をコマンド(オブジェクト)としてまとめているため、処理の履歴管理やUndo機能を実装しやすくなるというメリットもあります。

例題

Rubyによるデザインパターンの例を一部使用します。

  • ファイル作成
  • ファイル削除
  • ファイルコピー

の3つの機能を作ります。

  • コピー + 削除で「ファイル移動」
  • 作成 + コピーで「ファイル作成 & バックアップ」

の機能も作ります。

サンプルコード

Commandパターンは関数は第一級オブジェクトを使用するとかなり簡易に書くことができるので、サンプルコードでそのように書いています。

import os
import abc
import shutil

foldername1 = "__temp1"
foldername2 = "__temp2"


def main():
    # ①: move command
    print("step1: do nothing")
    print("folder1: ", os.listdir(foldername1))
    print("folder2: ", os.listdir(foldername2))

    # main command execute
    command_mv = CompositeCommand()
    command_mv.append_command(CopyCommand("__temp1/hello1.txt", "__temp2/hello1.txt"))
    command_mv.append_command(DeleteCommand("__temp1/hello1.txt"))

    print("step2: do move")
    command_mv.execute()
    print("folder1: ", os.listdir(foldername1))
    print("folder2: ", os.listdir(foldername2))

    print("step3: undo move")
    command_mv.unexecute()
    print("folder1: ", os.listdir(foldername1))
    print("folder2: ", os.listdir(foldername2))

    # ②: make and backup command
    print("step1: do nothing")
    print("folder1: ", os.listdir(foldername1))
    print("folder2: ", os.listdir(foldername2))

    command_make_and_backup = CompositeCommand()
    command_make_and_backup.append_command(CreateCommand("__temp1/hello2.txt", "hello2"))
    command_make_and_backup.append_command(CopyCommand("__temp1/hello2.txt", "__temp1/hello2.txt.backup"))

    print("step2: do make and backup")
    command_make_and_backup.execute()
    print("folder1: ", os.listdir(foldername1))
    print("folder2: ", os.listdir(foldername2))

    print("step3: undo make and backup")
    command_make_and_backup.unexecute()
    print("folder1: ", os.listdir(foldername1))
    print("folder2: ", os.listdir(foldername2))


# Command: 抽象クラス
class Command(metaclass=abc.ABCMeta):
    @abc.abstractmethod
    def execute(self):
        pass

    @abc.abstractmethod
    def unexecute(self):
        pass


# ConcreteCommand: 作成用のコマンド
class CreateCommand(Command):
    def __init__(self, path, contents):
        self.path = path
        self.contents = contents

    def execute(self):
        f = open(self.path, "w")
        f.write(self.contents)
        f.close()

    def unexecute(self):
        os.remove(self.path)


# ConcreteCommand: 削除用のコマンド
class DeleteCommand(Command):
    def __init__(self, path):
        self.path = path
        self.contents = None

    def execute(self):
        f = open(self.path, "r")
        self.contents = f.read()
        f.close()
        os.remove(self.path)

    def unexecute(self):
        if self.contents is not None:
            f = open(self.path, "w")
            f.write(self.contents)
            f.close()


# ConcreteCommand: コピー用のコマンド
class CopyCommand(Command):
    def __init__(self, path_from, path_to):
        self.path_from = path_from
        self.path_to = path_to

    def execute(self):
        shutil.copyfile(self.path_from, self.path_to)

    def unexecute(self):
        os.remove(self.path_to)


# Invoker(起動者)
class CompositeCommand(Command):
    def __init__(self):
        self.commands = []

    def append_command(self, cmd):
        self.commands.append(cmd)

    def execute(self):
        for cmd in self.commands:
            cmd.execute()

    def unexecute(self):
        for cmd in reversed(self.commands):
            cmd.unexecute()


if __name__ == "__main__":
    main()

# 出力

step1: do nothing folder1: ['hello1.txt'] folder2: [] step2: do move folder1: [] folder2: ['hello1.txt'] step3: undo move folder1: ['hello1.txt'] folder2: [] step1: do nothing folder1: ['hello1.txt'] folder2: [] step2: do make and backup folder1: ['hello2.txt.backup', 'hello2.txt', 'hello1.txt'] folder2: [] step3: undo make and backup folder1: ['hello1.txt'] folder2: []

特徴

大事なのは以下の部分です。

def main():
    ...
    command_mv = CompositeCommand()
    command_mv.append_command(CopyCommand("__temp1/hello1.txt", "__temp2/hello1.txt"))
    command_mv.append_command(DeleteCommand("__temp1/hello1.txt"))

    print("step2: do move")
    command_mv.execute()

...

# Invoker(起動者)
class CompositeCommand(Command):
    def __init__(self):
        self.commands = []

    def append_command(self, cmd):
        self.commands.append(cmd)

    def execute(self):
        for cmd in self.commands:
            cmd.execute()
    ...

Pythonの場合、関数は第一級オブジェクトのため、
コマンドを配列に入れ、それを順に実行(または逆順に実行でundo)ができます。

Pythonらしく書くためのコツ

サンプルコードで関数は第一級オブジェクトを使用しています。

クラス図

# 一般的なクラス図

一般的なクラス図(Commandパターン)

サンプルコードと違い、Receiver (受信者)がいます。
これは、Commandの処理対象となるオブジェクトのインタフェースなのですが、
Pythonの特性により、サンプルコードではReceiverを限定する必要がなくなっています。

# サンプルコードにおけるクラス図

サンプルコードにおけるクラス図(Commandパターン)

Pythonの特性により、Receiverを限定する必要がなくなっています。


参考URL

実践Python3

Wikipedia(クラス図をお借りしました)