第11章: Command パターン¶
はじめに¶
Command パターンは、リクエストをオブジェクトとしてカプセル化し、異なるリクエストでクライアントをパラメータ化したり、リクエストをキューに入れたり、操作の履歴を記録したりできるようにするパターンです。
本章では、テキスト編集とキャンバス操作の例を通じて、コマンドのデータ化、Undo/Redo 機能、バッチ処理の実装を学びます。
1. パターンの構造¶
Command パターンは以下の要素で構成されます:
- Command: コマンドのインターフェース(execute メソッド)
- ConcreteCommand: 具体的なコマンドの実装
- Receiver: コマンドを受け取り、実際の処理を行うオブジェクト
- Invoker: コマンドを実行するオブジェクト
2. Command インターフェース¶
マルチメソッドによる定義¶
(ns command-pattern.command)
(defmulti execute
"コマンドを実行する"
:type)
(defmulti undo
"コマンドを取り消す"
:type)
3. テキスト操作コマンド¶
InsertCommand: テキスト挿入¶
(ns command-pattern.text-commands
(:require [command-pattern.command :as cmd]))
(defn make-insert-command
"挿入コマンドを作成"
[position text]
{:type :insert-command
:position position
:text text})
(defmethod cmd/execute :insert-command [command]
(fn [document]
(let [pos (:position command)
text (:text command)
before (subs document 0 pos)
after (subs document pos)]
(str before text after))))
(defmethod cmd/undo :insert-command [command]
(fn [document]
(let [pos (:position command)
len (count (:text command))
before (subs document 0 pos)
after (subs document (+ pos len))]
(str before after))))
DeleteCommand: テキスト削除¶
(defn make-delete-command
"削除コマンドを作成"
[start-position end-position deleted-text]
{:type :delete-command
:start-position start-position
:end-position end-position
:deleted-text deleted-text})
(defmethod cmd/execute :delete-command [command]
(fn [document]
(let [start (:start-position command)
end (:end-position command)
before (subs document 0 start)
after (subs document end)]
(str before after))))
(defmethod cmd/undo :delete-command [command]
(fn [document]
(let [start (:start-position command)
text (:deleted-text command)
before (subs document 0 start)
after (subs document start)]
(str before text after))))
ReplaceCommand: テキスト置換¶
(defn make-replace-command
"置換コマンドを作成"
[start-position old-text new-text]
{:type :replace-command
:start-position start-position
:old-text old-text
:new-text new-text})
(defmethod cmd/execute :replace-command [command]
(fn [document]
(let [start (:start-position command)
old-len (count (:old-text command))
new-text (:new-text command)
before (subs document 0 start)
after (subs document (+ start old-len))]
(str before new-text after))))
(defmethod cmd/undo :replace-command [command]
(fn [document]
(let [start (:start-position command)
new-len (count (:new-text command))
old-text (:old-text command)
before (subs document 0 start)
after (subs document (+ start new-len))]
(str before old-text after))))
クラス図¶
使用例¶
(require '[command-pattern.command :as cmd])
(require '[command-pattern.text-commands :as tc])
;; 挿入コマンド
(def insert-cmd (tc/make-insert-command 5 " World"))
(def execute-fn (cmd/execute insert-cmd))
(execute-fn "Hello")
;; => "Hello World"
;; Undo
(def undo-fn (cmd/undo insert-cmd))
(undo-fn "Hello World")
;; => "Hello"
4. キャンバス操作コマンド¶
AddShapeCommand: 図形追加¶
(ns command-pattern.canvas-commands
(:require [command-pattern.command :as cmd]))
(defn make-add-shape-command
"図形追加コマンドを作成"
[shape]
{:type :add-shape-command
:shape shape})
(defmethod cmd/execute :add-shape-command [command]
(fn [canvas]
(update canvas :shapes conj (:shape command))))
(defmethod cmd/undo :add-shape-command [command]
(fn [canvas]
(update canvas :shapes
(fn [shapes]
(filterv #(not= (:id %) (get-in command [:shape :id])) shapes)))))
MoveShapeCommand: 図形移動¶
(defn make-move-shape-command
"図形移動コマンドを作成"
[shape-id dx dy]
{:type :move-shape-command
:shape-id shape-id
:dx dx
:dy dy})
(defmethod cmd/execute :move-shape-command [command]
(fn [canvas]
(update canvas :shapes
(fn [shapes]
(mapv (fn [shape]
(if (= (:id shape) (:shape-id command))
(-> shape
(update :x + (:dx command))
(update :y + (:dy command)))
shape))
shapes)))))
(defmethod cmd/undo :move-shape-command [command]
(fn [canvas]
(update canvas :shapes
(fn [shapes]
(mapv (fn [shape]
(if (= (:id shape) (:shape-id command))
(-> shape
(update :x - (:dx command))
(update :y - (:dy command)))
shape))
shapes)))))
5. コマンド実行器¶
CommandExecutor の実装¶
(ns command-pattern.command-executor
(:require [command-pattern.command :as cmd]))
(defn make-executor
"コマンド実行器を作成"
[initial-state]
{:state initial-state
:undo-stack []
:redo-stack []})
(defn execute-command
"コマンドを実行し、履歴に追加"
[executor command]
(let [state (:state executor)
execute-fn (cmd/execute command)
new-state (execute-fn state)]
(-> executor
(assoc :state new-state)
(update :undo-stack conj command)
(assoc :redo-stack []))))
(defn undo-command
"最後のコマンドを取り消す"
[executor]
(if (seq (:undo-stack executor))
(let [command (peek (:undo-stack executor))
state (:state executor)
undo-fn (cmd/undo command)
new-state (undo-fn state)]
(-> executor
(assoc :state new-state)
(update :undo-stack pop)
(update :redo-stack conj command)))
executor))
(defn redo-command
"最後に取り消したコマンドを再実行"
[executor]
(if (seq (:redo-stack executor))
(let [command (peek (:redo-stack executor))
state (:state executor)
execute-fn (cmd/execute command)
new-state (execute-fn state)]
(-> executor
(assoc :state new-state)
(update :redo-stack pop)
(update :undo-stack conj command)))
executor))
シーケンス図¶
使用例¶
(require '[command-pattern.command-executor :as exec])
(require '[command-pattern.text-commands :as tc])
;; 実行器を作成
(def executor (exec/make-executor "Hello"))
;; コマンドを実行
(def executor (exec/execute-command executor (tc/make-insert-command 5 " World")))
(exec/get-state executor)
;; => "Hello World"
;; Undo
(def executor (exec/undo-command executor))
(exec/get-state executor)
;; => "Hello"
;; Redo
(def executor (exec/redo-command executor))
(exec/get-state executor)
;; => "Hello World"
6. バッチ処理とマクロコマンド¶
バッチ処理¶
(defn execute-batch
"複数のコマンドをまとめて実行"
[executor commands]
(reduce execute-command executor commands))
(defn undo-all
"すべてのコマンドを取り消す"
[executor]
(if (can-undo? executor)
(recur (undo-command executor))
executor))
MacroCommand¶
(defn make-macro-command
"複合コマンドを作成"
[commands]
{:type :macro-command
:commands commands})
(defmethod cmd/execute :macro-command [macro]
(fn [state]
(reduce (fn [s command]
((cmd/execute command) s))
state
(:commands macro))))
(defmethod cmd/undo :macro-command [macro]
(fn [state]
(reduce (fn [s command]
((cmd/undo command) s))
state
(reverse (:commands macro)))))
MacroCommand の図¶
使用例¶
;; MacroCommand を使った複合操作
(def commands [(tc/make-insert-command 5 " World")
(tc/make-insert-command 11 "!")])
(def macro (exec/make-macro-command commands))
(def executor (exec/execute-command (exec/make-executor "Hello") macro))
(exec/get-state executor)
;; => "Hello World!"
;; 一度のUndoで全体を取り消す
(def executor (exec/undo-command executor))
(exec/get-state executor)
;; => "Hello"
7. パターンの利点¶
- 操作のオブジェクト化: 操作をデータとして表現できる
- Undo/Redo: 操作履歴の管理が容易
- バッチ処理: 複数の操作をまとめて処理
- 遅延実行: コマンドをキューに入れて後で実行
- ログ記録: 操作の履歴をログとして保存
8. 関数型プログラミングでの特徴¶
Clojure での Command パターンの実装には以下の特徴があります:
- コマンドはデータ: コマンドは単なるマップとして表現
- 純粋関数: execute/undo は状態変更関数を返す純粋関数
- イミュータブル: 状態は変更されず、新しい状態が返される
- マルチメソッド: 型に基づいたディスパッチで多態性を実現
まとめ¶
本章では、Command パターンについて学びました:
- コマンドのデータ化: 操作をマップとして表現
- Undo/Redo: コマンド履歴による取り消しと再実行
- バッチ処理: 複数コマンドの一括実行
- MacroCommand: 複合コマンドの実装
Command パターンは、操作の履歴管理や取り消し機能が必要な場面で非常に有効です。
参考コード¶
本章のコード例は以下のファイルで確認できます:
- ソースコード:
apps/clojure/part4/src/command_pattern/ - テストコード:
apps/clojure/part4/spec/command_pattern/
次章予告¶
次章では、Visitor パターンについて学びます。データ構造と操作を分離し、新しい操作を追加しやすくする方法を探ります。