tutoriales.com

Patrones de Diseño en Ruby: Implementando el Patrón Command para Tareas Robustas

Este tutorial práctico te enseña a dominar el Patrón de Diseño Command utilizando Ruby. Veremos cómo encapsular peticiones como objetos para lograr una arquitectura de software desacoplada, extensible y fácil de probar.

Intermedio9 min de lectura10 views
Reportar error

🚀 Introducción al Patrón Command en Ruby

En el desarrollo de software moderno, a menudo nos enfrentamos al desafío de diseñar sistemas donde los componentes que emiten una orden deben estar completamente desacoplados de los componentes que ejecutan dicha orden. ¿Cómo podemos lograr esto sin llenar nuestro código de condicionales complejos?

La respuesta se encuentra en los patrones de diseño orientados a objetos, específicamente en el Patrón Command. Este patrón encapsula una solicitud como un objeto, permitiéndote parametrizar clientes con diferentes solicitudes, encolar operaciones, registrar operaciones y soportar operaciones que pueden deshacerse (undo).

En este tutorial exhaustivo, exploraremos paso a paso cómo aplicar este patrón idiomáticamente en Ruby, aprovechando la naturaleza dinámica y flexible del lenguaje.

💡 Consejo: El Patrón Command es extremadamente útil para implementar interfaces de usuario con botones y atajos de teclado, sistemas de transacciones, colas de trabajos en segundo plano y operaciones que requieren la capacidad de deshacer y rehacer.

🛠️ ¿Qué es el Patrón Command y Cuándo Debemos Usarlo?

Para entender el Patrón Command, imaginemos un control remoto universal para el hogar. Cada botón del control remoto realiza una acción diferente (encender la luz, subir la temperatura del aire acondicionado, abrir la puerta del garaje). El control remoto no necesita saber cómo funciona internamente la luz o el aire acondicionado; simplemente envía una orden al objeto command asignado a ese botón.

El patrón se compone de cuatro elementos principales:

  1. Command (Comando): Una interfaz o clase abstracta que declara la ejecución de una operación.
  2. ConcreteCommand (Comando Concreto): Implementa el comando vinculando una acción con un Receiver (Receptor).
  3. Receiver (Receptor): El objeto que sabe cómo realizar la operación asociada a la solicitud.
  4. Invoker (Invocador): Pide al comando que ejecute la solicitud.
  5. Client (Cliente): Crea el objeto ConcreteCommand y establece su receptor.
Invocador setCommand() «interface» Command Comando Concreto execute() Receptor acción() Cliente

Ventajas Clave de usar Command

  • Desacoplamiento: Separa el objeto que invoca la operación del que sabe cómo realizarla.
  • Extensibilidad: Es muy fácil añadir nuevos comandos sin modificar el código existente (principio abierto/cerrado).
  • Composición: Puedes combinar varios comandos en una macro o cola de ejecución.
  • Deshacer/Rehacer: Facilita la implementación de la funcionalidad de deshacer operaciones guardando el estado previo.

💻 Implementando un Sistema Básico de Comandos en Ruby

Comenzaremos construyendo un ejemplo práctico: un sistema para controlar dispositivos inteligentes en una casa (luces y ventiladores).

Paso 1: Definir el Receptor (Receiver)

El receptor es la clase que contiene la lógica de negocio real. En nuestro caso, tendremos clases como Light y Fan.

class Light
  def turn_on
    puts "[Luz]: La luz está ENCENDIDA 💡"
  end

  def turn_off
    puts "[Luz]: La luz está APAGADA 🌑"
  end
end

class Fan
  def speed_high
    puts "[Ventilador]: Velocidad ALTA 🌀"
  end

  def speed_off
    puts "[Ventilador]: Ventilador APAGADO 🛑"
  end
end

Paso 2: Definir los Comandos Concretos

En Ruby, al ser un lenguaje de tipado dinámico, no necesitamos definir estrictamente interfaces formales, pero es una buena práctica asegurar que todos nuestros comandos respondan al método execute.

class LightOnCommand
  def initialize(light)
    @light = light
  end

  def execute
    @light.turn_on
  end
end

class LightOffCommand
  def initialize(light)
    @light = light
  end

  def execute
    @light.turn_off
  end
end

class FanHighCommand
  def initialize(fan)
    @fan = fan
  end

  def execute
    @fan.speed_high
  end
end

Paso 3: Crear el Invocador (Invoker)

El invocador será nuestro control remoto. Este objeto aceptará comandos y los ejecutará cuando sea necesario.

class RemoteControl
  def initialize
    @commands = {}
  end

  def set_command(slot, command)
    @commands[slot] = command
  end

  def press_button(slot)
    if @commands[slot]
      @commands[slot].execute
    else
      puts "Slot #{slot} no tiene ningún comando asignado."
    end
  end
end

Paso 4: Poniéndolo Todo Junto (Cliente)

Veamos cómo interactúan todas estas piezas en un script funcional de Ruby:

# Inicializar receptores
客厅_light = Light.new
bedroom_fan = Fan.new

# Crear comandos
light_on = LightOnCommand.new(客厅_light)
light_off = LightOffCommand.new(客厅_light)
fan_high = FanHighCommand.new(bedroom_fan)

# Configurar el invocador (Control Remoto)
remote = RemoteControl.new
remote.set_command(1, light_on)
remote.set_command(2, light_off)
remote.set_command(3, fan_high)

# Simular pulsaciones de botones
remote.press_button(1)
remote.press_button(3)
remote.press_button(2)
📌 Nota: Al ejecutar el script anterior, notarás cómo el control remoto no conoce los detalles de implementación de la luz ni del ventilador, logrando un diseño limpio y modular.

🔄 Añadiendo la Funcionalidad de Deshacer (Undo)

Una de las características más potentes del Patrón Command es la capacidad de revertir operaciones. Para lograr esto, cada comando debe implementar un método adicional, por ejemplo, unexecute o undo, y el invocador debe mantener un historial de los comandos ejecutados.

Modifiquemos nuestras clases para soportar esta funcionalidad.

Actualizando el Comando de la Luz

class DimmerLight
  attr_accessor 

  def initialize
     = 0
  end

  def set_brightness(level)
     = level
    puts "[Luz Regulable]: Brillo ajustado al %"
  end
end

class SetBrightnessCommand
  def initialize(light, new_brightness)
    @light = light
    @new_brightness = new_brightness
    @prev_brightness = nil
  end

  def execute
    @prev_brightness = @light.brightness
    @light.set_brightness(@new_brightness)
  end

  def undo
    puts "[Deshacer]: Reuniendo brillo anterior..."
    @light.set_brightness(@prev_brightness)
  end
end

Actualizando el Invocador con Historial

class AdvancedRemoteControl
  def initialize
    @commands = {}
    @history = []
  end

  def set_command(slot, command)
    @commands[slot] = command
  end

  def press_button(slot)
    if @commands[slot]
      @commands[slot].execute
      @history.push(@commands[slot])
    end
  end

  def press_undo
    if (last_command = @history.pop)
      last_command.undo
    else
      puts "No hay comandos para deshacer."
    end
  end
end

Probemos nuestro control avanzado:

my_light = DimmerLight.new
my_light.set_brightness(10) # Brillo inicial

command_50 = SetBrightnessCommand.new(my_light, 50)
command_100 = SetBrightnessCommand.new(my_light, 100)

remote = AdvancedRemoteControl.new
remote.set_command(1, command_50)
remote.set_command(2, command_100)

remote.press_button(1) # Brillo sube a 50
remote.press_button(2) # Brillo sube a 100

remote.press_undo   # Vuelve a 50
remote.press_undo   # Vuelve a 10

⚡ Idiomtic Ruby: Bloques y Callbacks como Comandos

Ruby es un lenguaje muy expresivo que nos permite simplificar patrones de diseño tradicionales mediante el uso de bloques de código (Procs y Lambdas). A menudo, no necesitamos crear una clase entera para cada comando si podemos pasar un bloque directamente.

Vamos a refactorizar nuestro control remoto para aceptar bloques de Ruby.

class FunctionalRemoteControl
  def initialize
    @commands = {}
  end

  def on_button(slot, &block)
    @commands[slot] = block
  end

  def press(slot)
    if @commands[slot]
      @commands[slot].call
    else
      puts "Acción no configurada."
    end
  end
end

# Uso práctico:
fr = FunctionalRemoteControl.new

fr.on_button(1) { puts "Ejecutando tarea compleja con bloques: ¡Cafetera encendida ☕!" }
fr.on_button(2) { puts "¡Sistema de seguridad armado 🔒!" }

fr.press(1)
fr.press(2)
⚠️ Advertencia: Aunque usar bloques y lambdas es extremadamente conciso y pythónico/rubístico, pierde la capacidad nativa de almacenar un historial complejo de estados previos para operaciones de tipo undo si no se maneja cuidadosamente con cierres (*closures*). Utiliza clases completas cuando necesites estructuras de comandos complejas.

📊 Comparativa: Clases de Comandos vs. Bloques en Ruby

Para ayudarte a decidir qué enfoque utilizar en tus proyectos de Ruby, revisa la siguiente tabla comparativa:

CaracterísticaClases ConcreteCommandBloques / Lambdas (Procs)Mejor opción para...
------------
ComplejidadAlta (Requiere más código boilerplate)Baja (Muy conciso)Tareas simples vs complejas
Soporte de UndoExcelente (Estado encapsulado internamente)Limitado (Requiere gestión manual de closures)Sistemas transaccionales
------------
SerializaciónFácil de guardar en bases de datos / colasImposible de serializar directamenteProcesos en segundo plano (Sidekiq)
LegibilidadMuy clara en proyectos grandesIdeal para scripts rápidos y DSLsMantenimiento a largo plazo

🔍 Preguntas Frecuentes (FAQ)

¿Cuándo debo evitar el Patrón Command? Evita usar el Patrón Command si tu aplicación es pequeña y directa, ya que introducir clases adicionales para cada acción puede generar sobreingeniería innecesaria.
¿Cómo se relaciona este patrón con Sidekiq u otras herramientas de background jobs en Ruby? Los gestores de colas de trabajos en Ruby como Sidekiq utilizan implícitamente una variante del Patrón Command. Cada clase de *Worker* actúa como un comando serializado que encapsula los datos y la lógica necesaria para ejecutarse asíncronamente en el futuro.

🎯 Conclusión

El Patrón Command es una herramienta arquitectónica indispensable en el arsenal de cualquier desarrollador de Ruby. Nos permite desacoplar emisores y receptores, simplificar la gestión de colas de tareas, implementar mecanismos robustos de deshacer y rehacer, y estructurar aplicaciones altamente escalables.

Ya sea que optes por la implementación clásica orientada a objetos mediante clases dedicadas o por la aproximación funcional utilizando bloques y lambdas, Ruby te ofrece todas las herramientas necesarias para escribir un código limpio, elegante y profesional.

¡Es hora de llevar este conocimiento a tus proyectos y experimentar con arquitecturas más desacopladas!

Tutoriales relacionados

Comentarios (0)

Aún no hay comentarios. ¡Sé el primero!