Command Pattern в Go: инкапсуляция запросов в объекты
Command Pattern превращает запросы в объекты. Вместо прямого вызова метода вы создаёте объект-команду, который инкапсулирует всю информацию о действии: что делать, с какими параметрами, на каком объекте.
Это как заказ в ресторане: официант не готовит еду сам, он записывает заказ на бумажке и передаёт на кухню. Бумажка - это команда. Её можно отложить, отменить, повторить или передать другому повару.
Зачем инкапсулировать запросы
Прямой вызов метода жёстко связывает отправителя и получателя:
button.OnClick(func() {
document.Save()
})
Проблемы такого подхода:
- Нельзя отменить действие
- Нельзя отложить выполнение
- Нельзя записать историю
- Нельзя передать действие в очередь
- Сложно логировать и отслеживать
Command Pattern решает эти проблемы, превращая действие в объект.
Базовая структура
type Command interface {
Execute() error
Undo() error
}
Команда знает, что делать и как отменить. Отправитель не знает деталей реализации.
Реальный пример 1: текстовый редактор с Undo/Redo
Классический пример - редактор с возможностью отмены действий. Каждое действие пользователя - это команда.
type Editor struct {
content string
cursor int
}
type InsertTextCommand struct {
editor *Editor
text string
position int
}
func (c *InsertTextCommand) Execute() error {
c.position = c.editor.cursor
before := c.editor.content[:c.position]
after := c.editor.content[c.position:]
c.editor.content = before + c.text + after
c.editor.cursor = c.position + len(c.text)
return nil
}
func (c *InsertTextCommand) Undo() error {
before := c.editor.content[:c.position]
after := c.editor.content[c.position+len(c.text):]
c.editor.content = before + after
c.editor.cursor = c.position
return nil
}
Команда удаления текста:
type DeleteTextCommand struct {
editor *Editor
deletedText string
position int
length int
}
func (c *DeleteTextCommand) Execute() error {
c.position = c.editor.cursor
c.deletedText = c.editor.content[c.position:c.position+c.length]
before := c.editor.content[:c.position]
after := c.editor.content[c.position+c.length:]
c.editor.content = before + after
return nil
}
func (c *DeleteTextCommand) Undo() error {
before := c.editor.content[:c.position]
after := c.editor.content[c.position:]
c.editor.content = before + c.deletedText + after
c.editor.cursor = c.position + len(c.deletedText)
return nil
}
История команд для Undo/Redo:
type CommandHistory struct {
commands []Command
current int
}
func (h *CommandHistory) Execute(cmd Command) error {
if err := cmd.Execute(); err != nil {
return err
}
// Удалить команды после текущей позиции
h.commands = h.commands[:h.current]
h.commands = append(h.commands, cmd)
h.current++
return nil
}
func (h *CommandHistory) Undo() error {
if h.current == 0 {
return errors.New("nothing to undo")
}
h.current--
return h.commands[h.current].Undo()
}
func (h *CommandHistory) Redo() error {
if h.current >= len(h.commands) {
return errors.New("nothing to redo")
}
cmd := h.commands[h.current]
h.current++
return cmd.Execute()
}
Использование:
editor := &Editor{}
history := &CommandHistory{}
// Пользователь вводит текст
history.Execute(&InsertTextCommand{
editor: editor,
text: "Hello",
})
history.Execute(&InsertTextCommand{
editor: editor,
text: " World",
})
// Отменить последнее действие
history.Undo() // Удалит " World"
// Вернуть обратно
history.Redo() // Вернёт " World"
Реальный пример 2: система задач и очередей
В распределённых системах команды используются для асинхронной обработки задач.
type TaskCommand interface {
Execute(ctx context.Context) error
GetID() string
GetPriority() int
}
type SendEmailCommand struct {
ID string
To string
Subject string
Body string
Priority int
}
func (c *SendEmailCommand) Execute(ctx context.Context) error {
// Отправка email
return emailService.Send(c.To, c.Subject, c.Body)
}
func (c *SendEmailCommand) GetID() string {
return c.ID
}
func (c *SendEmailCommand) GetPriority() int {
return c.Priority
}
Команда обработки изображения:
type ProcessImageCommand struct {
ID string
ImageURL string
Width int
Height int
Priority int
}
func (c *ProcessImageCommand) Execute(ctx context.Context) error {
// Скачать изображение
img, err := downloadImage(c.ImageURL)
if err != nil {
return err
}
// Изменить размер
resized := resize(img, c.Width, c.Height)
// Загрузить обратно
return uploadImage(resized)
}
func (c *ProcessImageCommand) GetID() string {
return c.ID
}
func (c *ProcessImageCommand) GetPriority() int {
return c.Priority
}
Очередь команд с приоритетами:
type CommandQueue struct {
commands []TaskCommand
mu sync.Mutex
workers int
}
func (q *CommandQueue) Add(cmd TaskCommand) {
q.mu.Lock()
defer q.mu.Unlock()
q.commands = append(q.commands, cmd)
sort.Slice(q.commands, func(i, j int) bool {
return q.commands[i].GetPriority() > q.commands[j].GetPriority()
})
}
func (q *CommandQueue) Process(ctx context.Context) {
for i := 0; i < q.workers; i++ {
go q.worker(ctx)
}
}
func (q *CommandQueue) worker(ctx context.Context) {
for {
cmd := q.next()
if cmd == nil {
time.Sleep(100 * time.Millisecond)
continue
}
if err := cmd.Execute(ctx); err != nil {
log.Printf("Command %s failed: %v", cmd.GetID(), err)
}
}
}
func (q *CommandQueue) next() TaskCommand {
q.mu.Lock()
defer q.mu.Unlock()
if len(q.commands) == 0 {
return nil
}
cmd := q.commands[0]
q.commands = q.commands[1:]
return cmd
}
Использование:
queue := &CommandQueue{workers: 5}
queue.Process(context.Background())
// Добавить задачи
queue.Add(&SendEmailCommand{
ID: "email-1",
To: "user@example.com",
Subject: "Welcome",
Body: "Hello!",
Priority: 10,
})
queue.Add(&ProcessImageCommand{
ID: "img-1",
ImageURL: "https://example.com/image.jpg",
Width: 800,
Height: 600,
Priority: 5,
})
Реальный пример 3: транзакции и откат изменений
В системах с базами данных команды используются для управления транзакциями.
type DatabaseCommand interface {
Execute(tx *sql.Tx) error
Rollback(tx *sql.Tx) error
}
type CreateUserCommand struct {
UserID string
Email string
Password string
}
func (c *CreateUserCommand) Execute(tx *sql.Tx) error {
_, err := tx.Exec(
"INSERT INTO users (id, email, password) VALUES ($1, $2, $3)",
c.UserID, c.Email, c.Password,
)
return err
}
func (c *CreateUserCommand) Rollback(tx *sql.Tx) error {
_, err := tx.Exec("DELETE FROM users WHERE id = $1", c.UserID)
return err
}
Команда создания профиля:
type CreateProfileCommand struct {
UserID string
Name string
Bio string
}
func (c *CreateProfileCommand) Execute(tx *sql.Tx) error {
_, err := tx.Exec(
"INSERT INTO profiles (user_id, name, bio) VALUES ($1, $2, $3)",
c.UserID, c.Name, c.Bio,
)
return err
}
func (c *CreateProfileCommand) Rollback(tx *sql.Tx) error {
_, err := tx.Exec("DELETE FROM profiles WHERE user_id = $1", c.UserID)
return err
}
Менеджер транзакций:
type TransactionManager struct {
db *sql.DB
commands []DatabaseCommand
}
func (m *TransactionManager) Execute(commands ...DatabaseCommand) error {
tx, err := m.db.Begin()
if err != nil {
return err
}
m.commands = commands
for _, cmd := range commands {
if err := cmd.Execute(tx); err != nil {
// Откатить все команды
for i := len(m.commands) - 1; i >= 0; i-- {
m.commands[i].Rollback(tx)
}
tx.Rollback()
return err
}
}
return tx.Commit()
}
Использование:
manager := &TransactionManager{db: db}
err := manager.Execute(
&CreateUserCommand{
UserID: "user-123",
Email: "user@example.com",
Password: "hashed",
},
&CreateProfileCommand{
UserID: "user-123",
Name: "John Doe",
Bio: "Software Engineer",
},
)
if err != nil {
// Все изменения откачены
}
Реальный пример 4: макросы и пакетные операции
Команды можно группировать для выполнения серии действий.
type MacroCommand struct {
commands []Command
}
func (m *MacroCommand) Execute() error {
for _, cmd := range m.commands {
if err := cmd.Execute(); err != nil {
return err
}
}
return nil
}
func (m *MacroCommand) Undo() error {
// Отменить в обратном порядке
for i := len(m.commands) - 1; i >= 0; i-- {
if err := m.commands[i].Undo(); err != nil {
return err
}
}
return nil
}
Пример: форматирование документа:
func FormatDocument(editor *Editor) Command {
return &MacroCommand{
commands: []Command{
&SelectAllCommand{editor: editor},
&SetFontCommand{editor: editor, font: "Arial"},
&SetSizeCommand{editor: editor, size: 12},
&AlignCommand{editor: editor, align: "left"},
},
}
}
// Использование
history.Execute(FormatDocument(editor))
// Отменить всё форматирование одной командой
history.Undo()
Реальный пример 5: планировщик задач
Команды с отложенным выполнением и повторами.
type ScheduledCommand struct {
command Command
executeAt time.Time
retry int
maxRetry int
}
type Scheduler struct {
commands []*ScheduledCommand
mu sync.Mutex
}
func (s *Scheduler) Schedule(cmd Command, delay time.Duration) {
s.mu.Lock()
defer s.mu.Unlock()
s.commands = append(s.commands, &ScheduledCommand{
command: cmd,
executeAt: time.Now().Add(delay),
maxRetry: 3,
})
}
func (s *Scheduler) Run(ctx context.Context) {
ticker := time.NewTicker(100 * time.Millisecond)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
s.processCommands()
}
}
}
func (s *Scheduler) processCommands() {
s.mu.Lock()
defer s.mu.Unlock()
now := time.Now()
remaining := make([]*ScheduledCommand, 0)
for _, sc := range s.commands {
if now.Before(sc.executeAt) {
remaining = append(remaining, sc)
continue
}
if err := sc.command.Execute(); err != nil {
sc.retry++
if sc.retry < sc.maxRetry {
sc.executeAt = now.Add(time.Second * time.Duration(sc.retry))
remaining = append(remaining, sc)
}
}
}
s.commands = remaining
}
Использование:
scheduler := &Scheduler{}
go scheduler.Run(context.Background())
// Отправить email через 5 минут
scheduler.Schedule(&SendEmailCommand{
To: "user@example.com",
Subject: "Reminder",
Body: "Don't forget!",
}, 5*time.Minute)
// Обработать изображение через час
scheduler.Schedule(&ProcessImageCommand{
ImageURL: "https://example.com/image.jpg",
Width: 1920,
Height: 1080,
}, time.Hour)
Реальный пример 6: логирование и аудит
Команды автоматически логируют все действия.
type LoggingCommand struct {
command Command
logger *log.Logger
userID string
}
func (c *LoggingCommand) Execute() error {
c.logger.Printf("User %s executing command: %T", c.userID, c.command)
start := time.Now()
err := c.command.Execute()
duration := time.Since(start)
if err != nil {
c.logger.Printf("Command failed after %v: %v", duration, err)
} else {
c.logger.Printf("Command succeeded in %v", duration)
}
return err
}
func (c *LoggingCommand) Undo() error {
c.logger.Printf("User %s undoing command: %T", c.userID, c.command)
return c.command.Undo()
}
Обёртка для добавления логирования:
func WithLogging(cmd Command, userID string, logger *log.Logger) Command {
return &LoggingCommand{
command: cmd,
logger: logger,
userID: userID,
}
}
// Использование
history.Execute(WithLogging(
&DeleteTextCommand{editor: editor, length: 5},
"user-123",
logger,
))
Когда использовать Command Pattern
- Undo/Redo - когда нужна история действий с возможностью отмены
- Очереди задач - когда действия нужно выполнять асинхронно
- Транзакции - когда нужен откат изменений при ошибке
- Логирование - когда нужно записывать все действия пользователя
- Макросы - когда нужно группировать команды
- Отложенное выполнение - когда действие нужно выполнить позже
Преимущества
- Разделение отправителя и получателя
- Возможность отмены и повтора
- Логирование и аудит
- Очереди и планирование
- Композиция команд
- Тестируемость
Недостатки
- Увеличение количества классов
- Дополнительная сложность для простых операций
- Overhead на создание объектов
Command vs Strategy
Strategy выбирает алгоритм. Command инкапсулирует действие.
// Strategy: выбор алгоритма
type SortStrategy interface {
Sort([]int) []int
}
// Command: действие с отменой
type Command interface {
Execute() error
Undo() error
}
Заключение
Command Pattern в Go:
- Превращает действия в объекты
- Позволяет отменять и повторять операции
- Упрощает логирование и аудит
- Поддерживает очереди и планирование
- Делает код более гибким и тестируемым
Команды - это не усложнение. Это мощный инструмент для управления действиями в приложении. Если вам нужна история, отмена, очереди или логирование - используйте Command Pattern.
В современных приложениях команды везде: текстовые редакторы, системы задач, транзакции, планировщики, аудит. Они делают сложные операции управляемыми.