Паттерн Команда
Часть раздела Объектно-ориентированное программирование путешествия по 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()) // Свет выключенПаттерн Команда идеально подходит для реализации систем отмены/повтора действий, очередей задач или записи макросов, когда вам необходимо сохранять, откладывать или повторно воспроизводить операции.
Задание
ЛегкоДавайте создадим текстовый редактор с функцией отмены (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)
}
В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.
Все уроки раздела Объектно-ориентированное программирование
1Основы ООП в Go
Внешние файлыРабочее пространство и модули GoПакеты и импортыЭкспортируемые и неэкспортируемые именаВведение в ООП в GoСтруктуры как классыОпределение методов структурПолучатели-указатели и получатели-значенияИнициализация структурФункции-конструкторыИтоги — Простой калькулятор4Интерфейсы
Введение в интерфейсыНеявная реализацияИнтерфейс как контрактПустой интерфейс (any)Утверждение типаПереключатель типовКомпозиция интерфейсовИнтерфейсы Stringer и ErrorПовторение: Калькулятор фигур7Инкапсуляция
Экспортируемые и неэкспортируемые поляИнкапсуляция на уровне пакетовГеттеры и сеттерыСокрытие информации в GoИтоги — Записи о студентах10Обобщения (Generics) (Go 1.18+)
Введение в GenericsПараметры типовОграничения типовОбобщенные структурыОбходной путь для обобщенных методовИтоги — Обобщенная коллекция13Паттерны проектирования. Часть 1
Введение в паттерны проектированияПаттерн SingletonПаттерн FactoryПаттерн Abstract FactoryПаттерн ObserverПаттерн Strategy2Глубокое погружение в типы и структуры
Базовые и составные типыОпределение пользовательских типовТеги структурАнонимные структурыВложенные структурыНулевые значения и значения по умолчаниюПовторение — Контактная книга5Композиция вместо наследования
Почему в Go нет наследованияОсновы встраивания структурПродвижение методовВстраивание нескольких структурВстраивание против агрегацииЗатенение встроенных методовИтоги — Иерархия сотрудников8Обработка ошибок и ООП
Интерфейс errorПользовательские типы ошибокОбертывание ошибок (fmt.Errorf)Sentinel-ошибкиerrors.Is() и errors.As()Panic, Defer и RecoverИтоги — Парсер файлов11Стандартная библиотека и ООП
io.Reader и io.Writersort.InterfaceИнтерфейс fmt.Stringerencoding/json со структурамиИнтерфейс http.HandlerПовторение: модели REST API14Паттерны проектирования. Часть 2
Паттерн КомандаПаттерн АдаптерПаттерн ДекораторПаттерн Шаблонный методПаттерн СостояниеПаттерн КомпоновщикMiddleware как Декоратор3Указатели и память
Основы указателей в GoУказатели на структурыПередача по значению и по ссылкеФункция new()Сборка мусора в GoПовторение: Конструктор связного списка6Полиморфизм в Go
Полиморфизм через интерфейсыУтиная типизация в GoПравила реализации интерфейсовПолиморфные коллекцииВнедрение зависимостейИтоги — Обработчик платежей9Конкурентность и ООП
Основы горутинКаналы и взаимодействиеБуферизованные и небуферизованные каналыОператор selectsync.Mutex и sync.RWMutexsync.WaitGroupПроектирование потокобезопасных структурПовторение — Worker Pool