Menu
Coddy logo textTech

Patrón Command

Parte de la sección Programación Orientada a Objetos del Journey de GO de Coddy — lección 91 de 107.

El patrón Command encapsula una solicitud como un objeto, lo que permite parametrizar operaciones, encolarlas o soportar la funcionalidad de deshacer. Mientras que Strategy encapsula algoritmos, Command encapsula acciones completas junto con sus parámetros.

En Go, definimos una interfaz Command con un método Execute, luego creamos comandos concretos que contienen toda la información necesaria para realizar una acción:

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

Un invocador almacena y ejecuta comandos sin saber qué hacen:

type RemoteControl struct {
    command Command
}

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

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

El invocador está completamente desacoplado del receptor (la Luz):

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

remote.SetCommand(&TurnOnCommand{light: light})
fmt.Println(remote.PressButton())  // La luz se encendió

remote.SetCommand(&TurnOffCommand{light: light})
fmt.Println(remote.PressButton())  // La luz se apagó

Command es ideal para implementar sistemas de deshacer/rehacer, colas de tareas o grabación de macros donde necesites almacenar, retrasar o reproducir operaciones.

challenge icon

Desafío

Fácil

¡Vamos a construir un editor de texto con funcionalidad de deshacer utilizando el patrón Command! Crearás comandos que encapsulan operaciones de texto, permitiendo que el editor ejecute acciones y las revierta—un caso de uso clásico que demuestra por qué los comandos son tan potentes.

Organizarás tu código en tres archivos:

  • command.go: Define tu interfaz de comando y los comandos concretos de edición de texto.

    Crea una interfaz Command con dos métodos: Execute() string para realizar la acción y Undo() string para revertirla.

    Implementa dos tipos de comandos que operen sobre un *TextEditor:

    • InsertCommand — contiene el puntero al editor y el texto a insertar. Execute añade el texto al contenido del editor y devuelve Inserted: [text]. Undo elimina ese texto del final y devuelve Undone insert: [text]
    • DeleteCommand — contiene el puntero al editor y un recuento de caracteres a eliminar del final. Execute elimina esos caracteres (almacenándolos para el undo) y devuelve Deleted: [removed text]. Undo los restaura y devuelve Undone delete: [restored text]
  • editor.go: Crea tu editor de texto (el receptor) y el invocador que gestiona el historial de comandos.

    Construye una estructura TextEditor con un campo Content (string) y métodos para Append(text string) y DeleteLast(count int) string (devuelve el texto eliminado).

    Construye una estructura EditorInvoker que contenga un slice de comandos ejecutados como historial. Añade estos métodos:

    • ExecuteCommand(cmd Command) string — ejecuta el comando, lo añade al historial y devuelve el resultado
    • UndoLast() string — elimina el último comando del historial, llama a su Undo y devuelve el resultado. Si el historial está vacío, devuelve Nothing to undo
  • main.go: Demuestra tu sistema de comandos con una serie de operaciones.

    Lee un recuento de operaciones. Para cada operación, lee el tipo de operación (insert, delete, o undo). Para insert, lee también el texto a insertar. Para delete, lee el recuento de caracteres. Ejecuta cada operación a través de tu invocador e imprime el resultado. Después de todas las operaciones, imprime el contenido final del editor como Final: [content].

Se proporcionarán las siguientes entradas:

  • Línea 1: Número de operaciones
  • Para cada operación: tipo de operación, luego datos adicionales si es necesario (texto para insert, recuento para delete)

Por ejemplo, dado:

5
insert
Hello
insert
 World
delete
3
undo
undo

Tu salida debería ser:

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

Y dado:

4
insert
Go
insert
Lang
undo
insert
!

Tu salida debería ser:

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

Y dado:

2
undo
insert
Test

Tu salida debería ser:

Nothing to undo
Inserted: Test
Final: Test

Observa cómo cada comando encapsula todo lo necesario tanto para realizar como para revertir su acción. El invocador no sabe qué hacen los comandos—¡simplemente los ejecuta y mantiene el historial para el soporte de deshacer!

Hoja de referencia

El patrón Command encapsula una solicitud como un objeto, lo que permite parametrizar operaciones, ponerlas en cola o admitir la funcionalidad de deshacer.

Define una interfaz Command con un método Execute:

type Command interface {
    Execute() string
}

Crea comandos concretos que contengan toda la información necesaria para realizar una acción:

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

Un invocador almacena y ejecuta comandos sin saber qué hacen:

type RemoteControl struct {
    command Command
}

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

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

Ejemplo de uso que muestra el desacoplamiento entre el invocador y el receptor:

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

Para la funcionalidad de deshacer, añade un método Undo a la interfaz Command y mantén un historial de comandos en el invocador.

Pruébalo tú mismo

package main

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

func main() {
	reader := bufio.NewReader(os.Stdin)
	
	// Leer el número de operaciones
	line, _ := reader.ReadString('\n')
	numOps, _ := strconv.Atoi(strings.TrimSpace(line))
	
	// Crear el editor y el invoker
	editor := &TextEditor{}
	invoker := &EditorInvoker{}
	
	for i := 0; i < numOps; i++ {
		// Leer el tipo de operación
		opLine, _ := reader.ReadString('\n')
		opType := strings.TrimSpace(opLine)
		
		var result string
		
		switch opType {
		case "insert":
			// Leer el texto a insertar
			textLine, _ := reader.ReadString('\n')
			text := strings.TrimSuffix(textLine, "\n")
			
			// TODO: Crear un InsertCommand y ejecutarlo a través del invoker
			_ = text
			_ = editor
			
		case "delete":
			// Leer la cantidad de caracteres a eliminar
			countLine, _ := reader.ReadString('\n')
			count, _ := strconv.Atoi(strings.TrimSpace(countLine))
			
			// TODO: Crear un DeleteCommand y ejecutarlo a través del invoker
			_ = count
			
		case "undo":
			// TODO: Llamar a UndoLast en el invoker
			
		}
		
		fmt.Println(result)
	}
	
	// Imprimir el contenido final
	fmt.Printf("Final: %s\n", editor.Content)
}
quiz iconPonte a prueba

Esta lección incluye un breve cuestionario. Empieza la lección para responderlo y registrar tu progreso.

Todas las lecciones de Programación Orientada a Objetos