Menu
Coddy logo textTech

Паттерн Команда

Часть раздела Объектно-ориентированное программирование путешествия по GO на Coddy — урок 91 из 107.

Паттерн Команда (Command) инкапсулирует запрос в виде объекта, позволяя параметризовать операции, ставить их в очередь или поддерживать функциональность отмены. В то время как Стратегия (Strategy) инкапсулирует алгоритмы, Команда инкапсулирует целые действия вместе с их параметрами.

В Go мы определяем интерфейс Command с методом Execute, затем создаем конкретные команды, которые содержат всю информацию, необходимую для выполнения действия:

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"
}

Инициатор (invoker) хранит и выполняет команды, не зная, что именно они делают:

type RemoteControl struct {
    command Command
}

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

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

Вызывающий объект (invoker) полностью отделен от получателя (Light):

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

remote.SetCommand(&TurnOnCommand{light: light})
fmt.Println(remote.PressButton())  // Свет включен

remote.SetCommand(&TurnOffCommand{light: light})
fmt.Println(remote.PressButton())  // Свет выключен

Паттерн Команда идеально подходит для реализации систем отмены/повтора действий, очередей задач или записи макросов, когда вам необходимо сохранять, откладывать или повторно воспроизводить операции.

challenge icon

Задание

Легко

Давайте создадим текстовый редактор с функцией отмены (undo), используя паттерн Команда (Command)! Вы создадите команды, которые инкапсулируют текстовые операции, позволяя редактору выполнять действия и отменять их — классический пример, демонстрирующий мощь команд.

Вы организуете свой код в трех файлах:

  • command.go: Определите интерфейс команды и конкретные команды редактирования текста.

    Создайте интерфейс Command с двумя методами: Execute() string для выполнения действия и Undo() string для его отмены.

    Реализуйте два типа команд, которые работают с *TextEditor:

    • InsertCommand — хранит указатель на редактор и текст для вставки. Execute добавляет текст к содержимому редактора и возвращает Inserted: [text]. Undo удаляет этот текст с конца и возвращает Undone insert: [text]
    • DeleteCommand — хранит указатель на редактор и количество символов для удаления с конца. Execute удаляет эти символы (сохраняя их для отмены) и возвращает Deleted: [removed text]. Undo восстанавливает их и возвращает Undone delete: [restored text]
  • editor.go: Создайте текстовый редактор (получатель) и инициатор (invoker), который управляет историей команд.

    Создайте структуру TextEditor с полем Content (string) и методами Append(text string) и DeleteLast(count int) string (возвращает удаленный текст).

    Создайте структуру EditorInvoker, которая хранит слайс выполненных команд в качестве истории. Добавьте следующие методы:

    • ExecuteCommand(cmd Command) string — выполняет команду, добавляет ее в историю и возвращает результат
    • UndoLast() string — удаляет последнюю команду из истории, вызывает ее Undo и возвращает результат. Если история пуста, возвращает Nothing to undo
  • main.go: Продемонстрируйте работу вашей системы команд с помощью серии операций.

    Считайте количество операций. Для каждой операции считайте тип операции (insert, delete или undo). Для insert также считайте текст для вставки. Для delete считайте количество символов. Выполните каждую операцию через ваш invoker и выведите результат. После всех операций выведите финальное содержимое редактора в формате 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

Обратите внимание, как каждая команда инкапсулирует все необходимое как для выполнения, так и для отмены своего действия. Инициатор (invoker) не знает, что делают команды — он просто выполняет их и ведет историю для поддержки отмены!

Шпаргалка

Паттерн Команда (Command) инкапсулирует запрос в виде объекта, позволяя параметризовать операции, ставить их в очередь или поддерживать функциональность отмены.

Определите интерфейс Command с методом Execute:

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"
}

Инициатор (invoker) хранит и выполняет команды, не зная, что именно они делают:

type RemoteControl struct {
    command Command
}

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

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

Пример использования, демонстрирующий слабую связь (decoupling) между инициатором и получателем:

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 и сохраняйте историю команд в инициаторе.

Попробуйте сами

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: Вызвать UndoLast в invoker
			
		}
		
		fmt.Println(result)
	}
	
	// Вывести итоговое содержимое
	fmt.Printf("Final: %s\n", editor.Content)
}
quiz iconПроверьте себя

В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.

Все уроки раздела Объектно-ориентированное программирование