Menu
Coddy logo textTech

Commandパターン

CoddyのGOジャーニー「オブジェクト指向プログラミング」セクションの一部 — レッスン 91/107。

Command パターンは、リクエストをオブジェクトとしてカプセル化することで、操作のパラメータ化、キューへの追加、または取り消し(undo)機能のサポートを可能にします。Strategy がアルゴリズムをカプセル化するのに対し、Command はアクション全体をそのパラメータと共にカプセル化します。

Goでは、Executeメソッドを持つCommandインターフェースを定義し、アクションの実行に必要なすべての情報を保持する具体的なコマンドを作成します:

type Command interface {
    Execute() string
}

type Light struct {
    IsOn bool
}

type TurnOnCommand struct {
    light *Light
}

func (c *TurnOnCommand) Execute() string {
    c.light.IsOn = true
    return "Light turned on"
}

type TurnOffCommand struct {
    light *Light
}

func (c *TurnOffCommand) Execute() string {
    c.light.IsOn = false
    return "Light turned off"
}

インボーカーは、コマンドの内容を知ることなく、コマンドを保持し実行します:

type RemoteControl struct {
    command Command
}

func (r *RemoteControl) SetCommand(c Command) {
    r.command = c
}

func (r *RemoteControl) PressButton() string {
    return r.command.Execute()
}

インボーカーはレシーバー(Light)から完全に切り離されています:

light := &Light{}
remote := &RemoteControl{}

remote.SetCommand(&TurnOnCommand{light: light})
fmt.Println(remote.PressButton())  // ライトが点灯しました

remote.SetCommand(&TurnOffCommand{light: light})
fmt.Println(remote.PressButton())  // ライトが消灯しました

Commandは、操作の保存、遅延、または再実行が必要な、元に戻す/やり直し(undo/redo)システム、タスクキュー、またはマクロ記録の実装に最適です。

challenge icon

チャレンジ

簡単

Commandパターンを使って、アンドゥ(元に戻す)機能を備えたテキストエディタを構築しましょう!テキスト操作をカプセル化したコマンドを作成することで、エディタがアクションを実行し、それを取り消すことができるようになります。これは、コマンドがいかに強力であるかを示す古典的なユースケースです。

コードは以下の3つのファイルに分けて構成します:

  • command.go: コマンドインターフェースと具体的なテキスト編集コマンドを定義します。

    アクションを実行する Execute() string と、それを取り消す Undo() string の2つのメソッドを持つ Command インターフェースを作成します。

    *TextEditor に対して操作を行う2つのコマンドタイプを実装します:

    • InsertCommand — エディタへのポインタと挿入するテキストを保持します。Executeはエディタのコンテンツにテキストを追加し、Inserted: [text] を返します。Undoはそのテキストを末尾から削除し、Undone insert: [text] を返します。
    • DeleteCommand — エディタへのポインタと末尾から削除する文字数を保持します。Executeはそれらの文字を削除し(アンドゥのために保存しておく)、Deleted: [removed text] を返します。Undoはそれらを復元し、Undone delete: [restored text] を返します。
  • editor.go: テキストエディタ(レシーバ)と、コマンドの履歴を管理するインボーカー(起動者)を作成します。

    Content フィールド(string)と、Append(text string) および DeleteLast(count int) string(削除されたテキストを返す)メソッドを持つ TextEditor 構造体を構築します。

    実行されたコマンドのスライスを履歴として保持する EditorInvoker 構造体を構築します。以下のメソッドを追加してください:

    • ExecuteCommand(cmd Command) string — コマンドを実行し、履歴に追加して、その結果を返します。
    • UndoLast() string — 履歴から最後のコマンドを取り出し、そのUndoを呼び出して、結果を返します。履歴が空の場合は、Nothing to undo を返します。
  • main.go: 一連の操作を通じてコマンドシステムを実演します。

    操作の回数を読み取ります。各操作について、操作タイプ(insertdelete、または undo)を読み取ります。insertの場合は挿入するテキストも読み取ります。deleteの場合は文字数を読み取ります。インボーカーを通じて各操作を実行し、結果を出力します。すべての操作が終わった後、最終的なエディタのコンテンツを Final: [content] として出力します。

以下の入力が提供されます:

  • 1行目:操作の数
  • 各操作:操作タイプ、必要に応じて追加データ(insertの場合はテキスト、deleteの場合はカウント)

例えば、以下の場合:

5
insert
Hello
insert
 World
delete
3
undo
undo

出力は以下のようになります:

Inserted: Hello
Inserted:  World
Deleted: rld
Undone delete: rld
Undone insert:  World
Final: Hello

また、以下の場合:

4
insert
Go
insert
Lang
undo
insert
!

出力は以下のようになります:

Inserted: Go
Inserted: Lang
Undone insert: Lang
Inserted: !
Final: Go!

また、以下の場合:

2
undo
insert
Test

出力は以下のようになります:

Nothing to undo
Inserted: Test
Final: Test

各コマンドが、アクションの実行と取り消しの両方に必要なすべてをどのようにカプセル化しているかに注目してください。インボーカーはコマンドが何をするかを知りません。ただコマンドを実行し、アンドゥをサポートするために履歴を維持するだけです!

チートシート

Command パターンは、リクエストをオブジェクトとしてカプセル化することで、操作のパラメータ化、キューへの追加、または取り消し(Undo)機能のサポートを可能にします。

Execute メソッドを持つ Command インターフェースを定義します:

type Command interface {
    Execute() string
}

アクションを実行するために必要なすべての情報を保持する具体的なコマンドを作成します:

type Light struct {
    IsOn bool
}

type TurnOnCommand struct {
    light *Light
}

func (c *TurnOnCommand) Execute() string {
    c.light.IsOn = true
    return "Light turned on"
}

type TurnOffCommand struct {
    light *Light
}

func (c *TurnOffCommand) Execute() string {
    c.light.IsOn = false
    return "Light turned off"
}

インボーカー(実行者)は、コマンドが何をするかを知ることなく、コマンドを保存して実行します:

type RemoteControl struct {
    command Command
}

func (r *RemoteControl) SetCommand(c Command) {
    r.command = c
}

func (r *RemoteControl) PressButton() string {
    return r.command.Execute()
}

インボーカーとレシーバーの間の疎結合を示す使用例:

light := &Light{}
remote := &RemoteControl{}

remote.SetCommand(&TurnOnCommand{light: light})
fmt.Println(remote.PressButton())  // Light turned on

remote.SetCommand(&TurnOffCommand{light: light})
fmt.Println(remote.PressButton())  // Light turned off

取り消し(Undo)機能については、Command インターフェースに Undo メソッドを追加し、インボーカーでコマンドの履歴を保持します。

自分で試してみよう

package main

import (
	"bufio"
	"fmt"
	"os"
	"strconv"
	"strings"
)

func main() {
	reader := bufio.NewReader(os.Stdin)
	
	// 操作の数を読み込む
	line, _ := reader.ReadString('\n')
	numOps, _ := strconv.Atoi(strings.TrimSpace(line))
	
	// editor と invoker を作成する
	editor := &TextEditor{}
	invoker := &EditorInvoker{}
	
	for i := 0; i < numOps; i++ {
		// 操作のタイプを読み込む
		opLine, _ := reader.ReadString('\n')
		opType := strings.TrimSpace(opLine)
		
		var result string
		
		switch opType {
		case "insert":
			// 挿入するテキストを読み込む
			textLine, _ := reader.ReadString('\n')
			text := strings.TrimSuffix(textLine, "\n")
			
			// TODO: InsertCommand を作成し、invoker を通じて実行する
			_ = text
			_ = editor
			
		case "delete":
			// 削除する文字数を読み込む
			countLine, _ := reader.ReadString('\n')
			count, _ := strconv.Atoi(strings.TrimSpace(countLine))
			
			// TODO: DeleteCommand を作成し、invoker を通じて実行する
			_ = count
			
		case "undo":
			// TODO: invoker の UndoLast を呼び出す
			
		}
		
		fmt.Println(result)
	}
	
	// 最終的な内容を出力する
	fmt.Printf("Final: %s\n", editor.Content)
}
quiz icon腕試し

このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。

オブジェクト指向プログラミングのすべてのレッスン