0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

全30回:静的と動的でどう違うのか、JavaとPythonで学ぶデザインパターン - Day 16 Commandパターン:リクエストをオブジェクト化して柔軟なシステムを構築する

Posted at

はじめに

皆さん、こんにちは!「JavaとPythonで比べるデザインパターン」シリーズの第16回目です。
今回は、処理をオブジェクトとしてカプセル化し、Undo/Redo機能やキューイングシステムを簡単に実装できるCommand(コマンド)パターンについて詳しく解説します。

Commandパターンとは?

Commandパターンは、リクエスト(処理要求)をオブジェクトとしてカプセル化する振る舞いパターンです。これにより、リクエストの**発行者(Invoker)実行者(Receiver)**を完全に分離し、システムの柔軟性と拡張性を大幅に向上させることができます。

身近な例:スマート家電のリモコンシステム

従来のリモコンシステムを考えてみましょう:

従来の直接制御方式

リモコン → 直接家電を制御
├── エアコンボタン → エアコンのon/off
├── テレビボタン → テレビのチャンネル変更
└── 照明ボタン → 照明の調光

Commandパターン適用後

リモコン → コマンド → 家電
├── ボタン1 → AirConditionerOnCommand → エアコン起動
├── ボタン2 → TVChannelChangeCommand → チャンネル変更
├── ボタン3 → LightDimCommand → 照明調光
└── 履歴機能、タイマー機能、マクロ機能が簡単に実装可能

この設計により、新しい家電の追加、複数の操作を組み合わせたマクロ機能、操作の取り消し機能などが簡単に実装できます。

Commandパターンの核心価値

1. 責任の分離(Separation of Concerns)

  • Invoker: コマンドを実行するタイミングを管理
  • Command: 実行すべき処理の詳細をカプセル化
  • Receiver: 実際の処理ロジックを実装

2. 実行制御の柔軟性

  • 遅延実行: コマンドを保存して後で実行
  • キューイング: コマンドを順番に実行
  • Undo/Redo: コマンドの実行と取り消し

3. 拡張性の向上

  • 新しいコマンドの追加が既存コードに影響しない
  • コマンドの組み合わせ(マクロ)が容易
  • ログ記録やトランザクション機能の実装が簡単

UMLクラス図による構造理解

Client
  ↓ creates
Command ←─── ConcreteCommand
  ↑              ↓ calls action on
Invoker        Receiver

構成要素:

  • Command: すべてのコマンドの共通インターフェース
  • ConcreteCommand: 具体的なコマンドの実装
  • Invoker: コマンドを保持し実行する
  • Receiver: 実際の処理を行う
  • Client: コマンドオブジェクトを作成・設定する

Javaでの実装:型安全性と明確な構造

Javaでは、インターフェースを活用してコマンドの契約を明確に定義し、型安全性を確保します。

実装例:高機能テキストエディタ

// コマンドインターフェース
interface Command {
    void execute();
    void undo(); // Undo機能のサポート
    String getDescription(); // ログ・履歴表示用
}

// 受信者:テキストエディタの実装
class TextEditor {
    private StringBuilder content;
    private int cursorPosition;
    
    public TextEditor() {
        this.content = new StringBuilder();
        this.cursorPosition = 0;
    }
    
    public void insertText(String text) {
        content.insert(cursorPosition, text);
        cursorPosition += text.length();
        System.out.println("Inserted: '" + text + "' at position " + 
                          (cursorPosition - text.length()));
    }
    
    public void deleteText(int length) {
        if (cursorPosition >= length) {
            content.delete(cursorPosition - length, cursorPosition);
            cursorPosition -= length;
            System.out.println("Deleted " + length + " characters");
        }
    }
    
    public void setCursorPosition(int position) {
        this.cursorPosition = Math.max(0, Math.min(position, content.length()));
        System.out.println("Cursor moved to position " + cursorPosition);
    }
    
    public String getContent() {
        return content.toString();
    }
    
    public int getCursorPosition() {
        return cursorPosition;
    }
}

// 具象コマンド:テキスト挿入
class InsertTextCommand implements Command {
    private final TextEditor editor;
    private final String text;
    private final int originalPosition;
    
    public InsertTextCommand(TextEditor editor, String text) {
        this.editor = editor;
        this.text = text;
        this.originalPosition = editor.getCursorPosition();
    }
    
    @Override
    public void execute() {
        editor.insertText(text);
    }
    
    @Override
    public void undo() {
        editor.setCursorPosition(originalPosition + text.length());
        editor.deleteText(text.length());
        editor.setCursorPosition(originalPosition);
    }
    
    @Override
    public String getDescription() {
        return "Insert '" + text + "' at position " + originalPosition;
    }
}

// 具象コマンド:テキスト削除
class DeleteTextCommand implements Command {
    private final TextEditor editor;
    private final int deleteLength;
    private final int originalPosition;
    private String deletedText; // Undo用に保存
    
    public DeleteTextCommand(TextEditor editor, int deleteLength) {
        this.editor = editor;
        this.deleteLength = deleteLength;
        this.originalPosition = editor.getCursorPosition();
    }
    
    @Override
    public void execute() {
        // Undo用に削除対象のテキストを保存
        int startPos = Math.max(0, originalPosition - deleteLength);
        int endPos = originalPosition;
        deletedText = editor.getContent().substring(startPos, endPos);
        editor.deleteText(deleteLength);
    }
    
    @Override
    public void undo() {
        if (deletedText != null) {
            editor.setCursorPosition(originalPosition - deleteLength);
            editor.insertText(deletedText);
        }
    }
    
    @Override
    public String getDescription() {
        return "Delete " + deleteLength + " characters from position " + originalPosition;
    }
}

// 呼び出し元:エディタマネージャー
class EditorManager {
    private final List<Command> history;
    private final List<Command> redoStack;
    
    public EditorManager() {
        this.history = new ArrayList<>();
        this.redoStack = new ArrayList<>();
    }
    
    public void executeCommand(Command command) {
        command.execute();
        history.add(command);
        redoStack.clear(); // 新しいコマンド実行時はRedoスタックをクリア
        System.out.println("Executed: " + command.getDescription());
    }
    
    public void undo() {
        if (!history.isEmpty()) {
            Command lastCommand = history.remove(history.size() - 1);
            lastCommand.undo();
            redoStack.add(lastCommand);
            System.out.println("Undid: " + lastCommand.getDescription());
        } else {
            System.out.println("Nothing to undo");
        }
    }
    
    public void redo() {
        if (!redoStack.isEmpty()) {
            Command redoCommand = redoStack.remove(redoStack.size() - 1);
            redoCommand.execute();
            history.add(redoCommand);
            System.out.println("Redid: " + redoCommand.getDescription());
        } else {
            System.out.println("Nothing to redo");
        }
    }
    
    public void showHistory() {
        System.out.println("Command History:");
        for (Command cmd : history) {
            System.out.println("- " + cmd.getDescription());
        }
    }
}

// 使用例
public class TextEditorDemo {
    public static void main(String[] args) {
        TextEditor editor = new TextEditor();
        EditorManager manager = new EditorManager();
        
        // コマンドの実行
        manager.executeCommand(new InsertTextCommand(editor, "Hello "));
        manager.executeCommand(new InsertTextCommand(editor, "World!"));
        manager.executeCommand(new DeleteTextCommand(editor, 6)); // "World!"を削除
        manager.executeCommand(new InsertTextCommand(editor, "Java!"));
        
        System.out.println("Current content: " + editor.getContent());
        
        // Undo/Redo機能のテスト
        manager.undo(); // "Java!"の挿入を取り消し
        manager.undo(); // "World!"の削除を取り消し
        System.out.println("After undo: " + editor.getContent());
        
        manager.redo(); // "World!"の削除を再実行
        System.out.println("After redo: " + editor.getContent());
        
        manager.showHistory();
    }
}

Javaの特徴と利点

  • 型安全性: コンパイル時にコマンドの契約違反を検出
  • 明確なインターフェース: Commandインターフェースによる統一された操作
  • 拡張性: 新しいコマンドクラスの追加が既存コードに影響しない
  • 保守性: 各コマンドの責任が明確で理解しやすい

Pythonでの実装:柔軟性と簡潔性を活かす

Pythonでは、関数の第一級オブジェクト特性とクロージャを活用して、より柔軟で簡潔な実装が可能です。

from functools import partial
from typing import Callable, List, Optional
import inspect

# 受信者:テキストエディタ
class TextEditor:
    def __init__(self):
        self._content = []
        self._cursor_position = 0
    
    def insert_text(self, text: str) -> None:
        self._content.insert(self._cursor_position, text)
        self._cursor_position += 1
        print(f"Inserted: '{text}' at position {self._cursor_position - 1}")
    
    def delete_text(self, count: int = 1) -> List[str]:
        deleted = []
        for _ in range(min(count, len(self._content) - self._cursor_position)):
            if self._cursor_position < len(self._content):
                deleted.append(self._content.pop(self._cursor_position))
        print(f"Deleted {len(deleted)} items: {deleted}")
        return deleted
    
    def move_cursor(self, position: int) -> int:
        old_pos = self._cursor_position
        self._cursor_position = max(0, min(position, len(self._content)))
        print(f"Moved cursor from {old_pos} to {self._cursor_position}")
        return old_pos
    
    def get_content(self) -> str:
        return ' '.join(self._content)
    
    def get_cursor_position(self) -> int:
        return self._cursor_position

# コマンドクラス(Undo機能付き)
class Command:
    def __init__(self, execute_func: Callable, undo_func: Callable = None, description: str = ""):
        self.execute_func = execute_func
        self.undo_func = undo_func
        self.description = description or self._generate_description(execute_func)
    
    def execute(self):
        return self.execute_func()
    
    def undo(self):
        if self.undo_func:
            return self.undo_func()
        else:
            print(f"Undo not available for: {self.description}")
    
    def can_undo(self) -> bool:
        return self.undo_func is not None
    
    def _generate_description(self, func: Callable) -> str:
        if hasattr(func, '__name__'):
            return f"Execute {func.__name__}"
        return "Execute command"

# エディタマネージャー(Pythonらしいアプローチ)
class EditorManager:
    def __init__(self, editor: TextEditor):
        self.editor = editor
        self.history: List[Command] = []
        self.redo_stack: List[Command] = []
    
    def execute_command(self, command: Command) -> None:
        result = command.execute()
        self.history.append(command)
        self.redo_stack.clear()
        print(f"Executed: {command.description}")
        return result
    
    def undo(self) -> None:
        if self.history:
            last_command = self.history.pop()
            if last_command.can_undo():
                last_command.undo()
                self.redo_stack.append(last_command)
                print(f"Undid: {last_command.description}")
            else:
                print(f"Cannot undo: {last_command.description}")
        else:
            print("Nothing to undo")
    
    def redo(self) -> None:
        if self.redo_stack:
            redo_command = self.redo_stack.pop()
            redo_command.execute()
            self.history.append(redo_command)
            print(f"Redid: {redo_command.description}")
        else:
            print("Nothing to redo")
    
    def show_history(self) -> None:
        print("Command History:")
        for cmd in self.history:
            undo_status = "" if cmd.can_undo() else ""
            print(f"  {undo_status} {cmd.description}")

# コマンドファクトリー関数(Pythonらしいアプローチ)
def create_insert_command(editor: TextEditor, text: str) -> Command:
    original_pos = editor.get_cursor_position()
    
    def execute():
        editor.move_cursor(original_pos)
        editor.insert_text(text)
    
    def undo():
        editor.move_cursor(original_pos + 1)
        editor.delete_text(1)
        editor.move_cursor(original_pos)
    
    return Command(execute, undo, f"Insert '{text}' at position {original_pos}")

def create_delete_command(editor: TextEditor, count: int = 1) -> Command:
    original_pos = editor.get_cursor_position()
    deleted_items = []
    
    def execute():
        nonlocal deleted_items
        editor.move_cursor(original_pos)
        deleted_items = editor.delete_text(count)
    
    def undo():
        editor.move_cursor(original_pos)
        for item in reversed(deleted_items):
            editor.insert_text(item)
        editor.move_cursor(original_pos)
    
    return Command(execute, undo, f"Delete {count} items from position {original_pos}")

def create_move_command(editor: TextEditor, new_position: int) -> Command:
    original_pos = editor.get_cursor_position()
    
    def execute():
        editor.move_cursor(new_position)
    
    def undo():
        editor.move_cursor(original_pos)
    
    return Command(execute, undo, f"Move cursor from {original_pos} to {new_position}")

# マクロコマンド(複数コマンドの組み合わせ)
def create_macro_command(commands: List[Command], description: str = "Macro") -> Command:
    def execute():
        for cmd in commands:
            cmd.execute()
    
    def undo():
        # 逆順でUndo実行
        for cmd in reversed(commands):
            if cmd.can_undo():
                cmd.undo()
    
    return Command(execute, undo, description)

# 使用例
if __name__ == "__main__":
    editor = TextEditor()
    manager = EditorManager(editor)
    
    # 基本的なコマンド実行
    manager.execute_command(create_insert_command(editor, "Hello"))
    manager.execute_command(create_insert_command(editor, "World"))
    manager.execute_command(create_insert_command(editor, "Python"))
    
    print(f"Content: {editor.get_content()}")
    
    # 削除コマンド
    manager.execute_command(create_delete_command(editor, 1))
    print(f"After deletion: {editor.get_content()}")
    
    # Undo/Redo
    manager.undo()
    print(f"After undo: {editor.get_content()}")
    
    manager.redo()
    print(f"After redo: {editor.get_content()}")
    
    # マクロコマンドの実行
    macro = create_macro_command([
        create_move_command(editor, 0),
        create_insert_command(editor, "START:"),
        create_move_command(editor, editor.get_cursor_position() + len(editor.get_content().split()))
    ], "Add START prefix and move to end")
    
    manager.execute_command(macro)
    print(f"After macro: {editor.get_content()}")
    
    # 履歴表示
    manager.show_history()
    
    # 関数型アプローチの例(より簡潔)
    print("\n=== Function-based approach ===")
    
    # シンプルなコマンド実行(Undo機能なし)
    simple_commands = [
        lambda: editor.insert_text("Simple"),
        lambda: editor.insert_text("Commands"),
        partial(editor.delete_text, 1)
    ]
    
    for cmd in simple_commands:
        cmd()
    
    print(f"Final content: {editor.get_content()}")

Pythonの特徴と利点

  • 関数オブジェクト: 関数を直接コマンドとして使用可能
  • クロージャ: 状態を保持したコマンドを簡潔に作成
  • 柔軟性: ラムダ式、partial、デコレータなど多様な実装方法
  • 動的性: 実行時にコマンドの組み合わせや変更が容易

実践的な応用場面

1. GUI アプリケーション

# ボタンクリックイベントのハンドリング
button.on_click = Command(save_document, undo_save, "Save Document")

2. バッチ処理システム

// ジョブキューの実装
public class JobQueue {
    private Queue<Command> jobs = new LinkedList<>();
    
    public void addJob(Command job) {
        jobs.offer(job);
    }
    
    public void processAllJobs() {
        while (!jobs.isEmpty()) {
            jobs.poll().execute();
        }
    }
}

3. トランザクション処理

def create_transaction_command(operations):
    def execute():
        for op in operations:
            op.execute()
    
    def rollback():
        for op in reversed(operations):
            op.undo()
    
    return Command(execute, rollback, "Transaction")

パフォーマンスとメモリ使用量の考慮

Java

  • メモリ使用量: 各コマンドがオブジェクトとして作成されるため、大量のコマンドではメモリ消費が増加
  • パフォーマンス: 仮想メソッド呼び出しのオーバーヘッドあり
  • 最適化: Command Pool パターンでオブジェクトの再利用が可能

Python

  • メモリ使用量: クロージャは外部変数への参照を保持するため、メモリ使用量に注意
  • パフォーマンス: 動的ディスパッチにより若干のオーバーヘッド
  • 最適化: __slots__ の使用やジェネレーター関数の活用

まとめ:柔軟な制御構造の実現

特性 Java Python
実装方式 インターフェース + 具象クラス 関数オブジェクト + クロージャ
型安全性 コンパイル時チェックあり 実行時チェック(型ヒント使用推奨)
コード量 比較的多い(明示的構造) 簡潔(関数型アプローチ活用)
拡張性 新しいクラス追加で対応 関数追加やデコレータで対応
Undo機能 インターフェースで統一実装 クロージャで状態保持

Commandパターンは、 「処理を第一級オブジェクトとして扱う」 ことで、システムの柔軟性と拡張性を大幅に向上させます。JavaとPythonでは実装アプローチが異なりますが、どちらも実際のビジネスアプリケーションで威力を発揮する実用的なパターンです。

特に、現代のアプリケーション開発では、ユーザーエクスペリエンスの向上のためにUndo/Redo機能が重要になっており、Commandパターンはその実装において不可欠な設計パターンとなっています。

明日は、アルゴリズムを動的に切り替えるStrategyパターンについて詳しく解説します。ビジネスロジックの柔軟な切り替えを実現する設計技法をお楽しみに!


次回のテーマ:「Day 17 Strategyパターン:ビジネスロジックを動的に切り替える設計技法」

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?