tutoriales.com

¡Desatando el Potencial! Explorando los Decoradores de Métodos con `Module#prepend` en Ruby

Este tutorial explora a fondo cómo utilizar `Module#prepend` en Ruby para crear decoradores de métodos, una técnica poderosa para extender o modificar el comportamiento de métodos existentes sin alterar su código original. Cubriremos los fundamentos, casos de uso, ejemplos prácticos y consideraciones importantes para dominar esta característica.

Intermedio20 min de lectura7 views
Reportar error

🚀 Introducción a los Decoradores de Métodos en Ruby

En el mundo de la programación, a menudo nos encontramos con la necesidad de extender o modificar el comportamiento de métodos existentes sin tocar su código fuente original. Esto es especialmente útil cuando trabajamos con librerías de terceros, clases que no podemos modificar directamente, o simplemente queremos mantener un código base más limpio y modular. Aquí es donde entran en juego los decoradores de métodos.

Ruby, con su flexibilidad y naturaleza dinámica, ofrece varias maneras de lograr esto. Una de las más elegantes y poderosas, introducida en Ruby 2.0, es el método Module#prepend. A diferencia de Module#include, que inserta un módulo en la cadena de ancestros de una clase después de la clase misma, prepend lo inserta antes. Esta sutil pero crucial diferencia nos permite interceptar llamadas a métodos y "decorarlos" con funcionalidad adicional.

¿Qué son los Decoradores de Métodos?

Un decorador de métodos es un patrón de diseño que permite añadir nuevas funcionalidades o modificar el comportamiento de un método existente, de forma transparente, sin cambiar su estructura. Piensa en ello como "envolver" un método con lógica adicional. Esto es muy valioso para:

  • Añadir logging: Registrar cuándo se llama un método, con qué argumentos y qué devuelve.
  • Cacheo: Guardar el resultado de operaciones costosas para evitar recálculos.
  • Autorización/Autenticación: Verificar permisos antes de ejecutar un método.
  • Validación de argumentos: Asegurarse de que los parámetros cumplen ciertas condiciones.
  • Manejo de errores: Envolver el método en un bloque begin/rescue.
  • Instrumentación: Medir el tiempo de ejecución de un método.
💡 Consejo: Los decoradores promueven el principio de responsabilidad única (SRP), permitiéndote separar la lógica de negocio central de la lógica transversal como el logging o el cacheo.

📖 Entendiendo Module#prepend

Para comprender los decoradores de métodos con prepend, primero debemos entender cómo funciona la cadena de ancestros en Ruby y cómo prepend la modifica.

La Cadena de Ancestros (ancestors)

Cuando se llama a un método en Ruby, el intérprete busca ese método en la clase del objeto, luego en sus módulos incluidos (include), luego en su superclase, y así sucesivamente, subiendo por la cadena de ancestros hasta BasicObject. Puedes ver la cadena de ancestros de cualquier clase o módulo usando el método ancestors.

Veamos un ejemplo básico:

module MiModulo
  def saludar
    "Hola desde MiModulo"
  end
end

class MiClase
  include MiModulo

  def saludar
    "Hola desde MiClase"
  end
end

puts MiClase.ancestors.inspect # => [MiClase, MiModulo, Object, Kernel, BasicObject]

objeto = MiClase.new
puts objeto.saludar # => "Hola desde MiClase"

En este caso, MiClase implementa saludar, por lo que esa es la versión que se ejecuta. Si MiClase no tuviera saludar, se ejecutaría la versión de MiModulo.

Cómo prepend Cambia el Juego

Ahora, veamos qué sucede cuando usamos prepend:

module MiModuloPrepend
  def saludar
    "Hola desde MiModuloPrepend"
  end
end

class MiClasePrepend
  prepend MiModuloPrepend

  def saludar
    "Hola desde MiClasePrepend"
  end
end

puts MiClasePrepend.ancestors.inspect # => [MiModuloPrepend, MiClasePrepend, Object, Kernel, BasicObject]

objeto = MiClasePrepend.new
puts objeto.saludar # => "Hola desde MiModuloPrepend"

¡La cadena de ancestros ha cambiado! MiModuloPrepend ahora se encuentra antes de MiClasePrepend. Cuando objeto.saludar es llamado, Ruby primero busca en MiModuloPrepend, encuentra el método saludar allí y lo ejecuta. La implementación en MiClasePrepend es efectivamente ocultada por la versión del módulo prepended.

El Poder de super con prepend

Aquí es donde prepend se vuelve verdaderamente poderoso para los decoradores. Dentro de un método definido en un módulo prepended, podemos llamar a super para invocar la implementación original del método que está más abajo en la cadena de ancestros (es decir, el método en la clase o en otro módulo incluido).

module LoggingDecorator
  def hacer_algo(param)
    puts "[LOG] Iniciando hacer_algo con parámetro: #{param}"
    resultado = super(param) # Llama al método original de la clase
    puts "[LOG] Finalizado hacer_algo, resultado: #{resultado}"
    resultado
  end
end

class Servicio
  def hacer_algo(param)
    puts "  --> Ejecutando lógica de Servicio para: #{param}"
    "Resultado de Servicio para #{param.upcase}"
  end
end

Servicio.prepend LoggingDecorator

mi_servicio = Servicio.new
mi_servicio.hacer_algo("dato importante")

# Salida esperada:
# [LOG] Iniciando hacer_algo con parámetro: dato importante
#   --> Ejecutando lógica de Servicio para: dato importante
# [LOG] Finalizado hacer_algo, resultado: Resultado de Servicio para DATO IMPORTANTE

Este ejemplo ilustra perfectamente cómo LoggingDecorator "envuelve" el método hacer_algo de Servicio, añadiendo lógica de logging antes y después de la ejecución del método original, sin modificar la clase Servicio.

⚠️ Advertencia: Ten cuidado al usar `prepend` si la clase original define el método con una lista de argumentos diferente a la del módulo prepended. Esto puede llevar a `ArgumentError` si `super` se llama con argumentos que no coinciden con la definición del método original.

🛠️ Implementando Decoradores Prácticos con Module#prepend

Ahora que entendemos la teoría, veamos algunos casos de uso comunes y cómo implementarlos.

1. Decorador de Logging y Auditoría 📝

Este es el ejemplo más directo. Queremos registrar cada vez que se invoca un método importante y qué hace.

module AuditLogger
  def self.prepended(base)
    base.define_method(:method_added) do |method_name|
      # Evita decorar métodos que no queremos o los que ya hemos decorado
      return if method_name == :method_added || method_name == :audit_log || method_name.to_s.start_with?('original_')

      unless instance_methods(false).include?("original_#{method_name}".to_sym)
        alias_method "original_#{method_name}".to_sym, method_name
        define_method(method_name) do |*args, &block|
          puts "[AUDIT] Calling #{method_name} with args: #{args.inspect}"
          result = send("original_#{method_name}".to_sym, *args, &block)
          puts "[AUDIT] #{method_name} returned: #{result.inspect}"
          result
        end
      end
    end
  end
end

class UserAuthenticator
  def initialize(user, password)
    @user = user
    @password = password
  end

  def authenticate
    puts "  Authenticating user #{@user}..."
    sleep(0.5) # Simula una operación costosa
    @user == 'admin' && @password == 'password123'
  end

  def reset_password(new_password)
    puts "  Resetting password for #{@user}..."
    sleep(0.3)
    @password = new_password
    true
  end
end

UserAuthenticator.prepend AuditLogger

# Para que funcione `method_added`, debemos definir los métodos *después* de hacer `prepend`,
# o usar una técnica diferente para decorar métodos ya existentes.
# El ejemplo con `method_added` es más complejo. Volvamos al enfoque simple de `super`.

module SimpleAuditLogger
  def authenticate
    puts "[AUDIT] Iniciando autenticación para usuario."
    result = super
    puts "[AUDIT] Autenticación finalizada, resultado: #{result}."
    result
  end

  def reset_password(new_password)
    puts "[AUDIT] Iniciando reseteo de contraseña para usuario."
    result = super(new_password)
    puts "[AUDIT] Reseteo de contraseña finalizado, resultado: #{result}."
    result
  end
end

class UserAuthenticatorSimple
  def initialize(user, password)
    @user = user
    @password = password
  end

  def authenticate
    puts "  Authenticating user #{@user}..."
    sleep(0.5) # Simula una operación costosa
    @user == 'admin' && @password == 'password123'
  end

  def reset_password(new_password)
    puts "  Resetting password for #{@user}..."
    sleep(0.3)
    @password = new_password
    true
  end
end

UserAuthenticatorSimple.prepend SimpleAuditLogger

auth = UserAuthenticatorSimple.new('admin', 'password123')
auth.authenticate
auth.reset_password('new_secure_pass')

# Salida:
# [AUDIT] Iniciando autenticación para usuario.
#   Authenticating user admin...
# [AUDIT] Autenticación finalizada, resultado: true.
# [AUDIT] Iniciando reseteo de contraseña para usuario.
#   Resetting password for admin...
# [AUDIT] Reseteo de contraseña finalizado, resultado: true.

2. Decorador de Cacheo 💾

Ideal para métodos que realizan cálculos costosos y sus resultados no cambian frecuentemente para los mismos argumentos.

module CacheDecorator
  def calcular_datos_costosos(id)
    # Generar una clave de caché única basada en el nombre del método y los argumentos
    cache_key = "#{__method__}-#{id}"
    
    # Intentar recuperar de la caché
    if @cache && @cache.key?(cache_key)
      puts "[CACHE] Obteniendo de caché para ID: #{id}"
      return @cache[cache_key]
    end

    puts "[CACHE] Calculando y cacheando para ID: #{id}"
    # Inicializar caché si no existe
    @cache ||= {}

    # Llamar al método original y almacenar el resultado
    result = super(id)
    @cache[cache_key] = result
    result
  end
end

class DataProcessor
  def calcular_datos_costosos(id)
    puts "  Realizando cálculos complejos para ID: #{id}"
    sleep(1) # Simula un cálculo intensivo
    { id: id, data: "Calculado a las #{Time.now}" }
  end
end

DataProcessor.prepend CacheDecorator

processor = DataProcessor.new

puts "--- Primera llamada ---"
puts processor.calcular_datos_costosos(1)
puts processor.calcular_datos_costosos(2)

puts "--- Segunda llamada (debería usar caché) ---"
puts processor.calcular_datos_costosos(1)
puts processor.calcular_datos_costosos(2)

puts "--- Tercera llamada (nuevo ID) ---"
puts processor.calcular_datos_costosos(3)

# Salida esperada:
# --- Primera llamada ---
# [CACHE] Calculando y cacheando para ID: 1
#   Realizando cálculos complejos para ID: 1
# {:id=>1, :data=>"Calculado a las ..."}
# [CACHE] Calculando y cacheando para ID: 2
#   Realizando cálculos complejos para ID: 2
# {:id=>2, :data=>"Calculado a las ..."}
# --- Segunda llamada (debería usar caché) ---
# [CACHE] Obteniendo de caché para ID: 1
# {:id=>1, :data=>"Calculado a las ..."}
# [CACHE] Obteniendo de caché para ID: 2
# {:id=>2, :data=>"Calculado a las ..."}
# --- Tercera llamada (nuevo ID) ---
# [CACHE] Calculando y cacheando para ID: 3
#   Realizando cálculos complejos para ID: 3
# {:id=>3, :data=>"Calculado a las ..."}

3. Decorador de Tiempo de Ejecución ⏱️

Útil para perfilar y entender cuánto tiempo toma la ejecución de un método.

module TimeMeasurementDecorator
  def realizar_tarea_larga
    start_time = Time.now
    puts "[TIMER] Iniciando tarea larga..."
    result = super
    end_time = Time.now
    duration = end_time - start_time
    puts "[TIMER] Tarea finalizada en #{duration.round(4)} segundos."
    result
  end
end

class TaskRunner
  def realizar_tarea_larga
    puts "  Ejecutando lógica de negocio intensiva."
    sleep(rand(0.5..1.5)) # Simula trabajo variable
    "Tarea completa con éxito!"
  end
end

TaskRunner.prepend TimeMeasurementDecorator

r = TaskRunner.new
r.realizar_tarea_larga
📌 Nota: Los decoradores pueden encadenarse. Si `ModuleA` se antepone a `ClassX`, y luego `ModuleB` se antepone a `ClassX` (o a `ModuleA`), el módulo más recientemente prepended estará más arriba en la cadena de ancestros.

🤯 Consideraciones Avanzadas y Mejores Prácticas

Aunque prepend es una herramienta poderosa, su uso requiere comprensión y consideración para evitar problemas.

Cuándo Usar prepend vs. include vs. extend

Es crucial entender las diferencias:

Característicaincludeprependextend
Posición en ancestorsDespués de la claseAntes de la claseNo afecta ancestors de la clase
Métodos afectadosMétodos de instancia (mezclados)Métodos de instancia (decoradores)Métodos de clase (singelton class)
Uso principalCompartir funcionalidades, herencia mixinDecorar métodos existentes, interceptar llamadasAñadir métodos de clase o clase-singleton
Llamada a superLlama a la superclaseLlama al método original de la claseNo aplica (no son métodos de instancia)
Cadena de Ancestros en Ruby Uso de include Clase Módulo Incluido Superclase Uso de prepend Módulo Prepended Clase Superclase Clase Base Módulo Jerarquía Superior

Manejo de Argumentos con *args y &block

Cuando decoras métodos con prepend, es una buena práctica pasar *args y &block a super para asegurar que el método original reciba todos los argumentos y el bloque si los espera. Esto hace que tu decorador sea más robusto y genérico.

module GenericDecorator
  def decorated_method(*args, &block)
    puts "[DECORATOR] Antes de ejecutar #{__method__} con args: #{args.inspect}"
    result = super(*args, &block) # Pasando todos los argumentos y el bloque
    puts "[DECORATOR] Después de ejecutar #{__method__}, resultado: #{result.inspect}"
    result
  end
end

class TargetClass
  def decorated_method(a, b, &block)
    puts "  --> Ejecutando TargetClass#decorated_method con a=#{a}, b=#{b}"
    block.call("Bloque ejecutado") if block_given?
    a + b
  end
end

TargetClass.prepend GenericDecorator

t = TargetClass.new
t.decorated_method(5, 10) { |msg| puts "  Bloque recibió: #{msg}" }

# Salida:
# [DECORATOR] Antes de ejecutar decorated_method con args: [5, 10]
#   --> Ejecutando TargetClass#decorated_method con a=5, b=10
#   Bloque recibió: Bloque ejecutado
# [DECORATOR] Después de ejecutar decorated_method, resultado: 15

Orden de prepend y alias_method

Si necesitas decorar métodos que ya existen y no quieres redefinir cada método manualmente dentro del decorador (como en el SimpleAuditLogger anterior), puedes usar una técnica más dinámica. Sin embargo, esto a menudo implica alias_method, lo que puede volverse complejo con prepend si no se maneja correctamente, ya que prepend afecta directamente la resolución de super.

Una forma de decorar todos los métodos de una clase con un módulo prepend es usar Module#method_added:

Ejemplo avanzado: Decorador genérico con `method_added` ```ruby module GenericMethodDecorator def self.prepended(base) base.singleton_class.prepend(ClassMethods) end

module ClassMethods def method_added(method_name) return if method_name == :method_added || method_name.to_s.start_with?('_decorated')

  # Si el método ya ha sido decorado, no lo decoremos de nuevo.
  # Esto es para evitar recursión infinita cuando redefine_method es llamado.
  return if instance_methods(false).include?("__decorated_#{method_name}".to_sym)

  original_method = instance_method(method_name)

  define_method("__decorated_#{method_name}", original_method)

  define_method(method_name) do |*args, &block|
    puts "[GENERIC DECORATOR] Antes de #{method_name} en #{self.class}."
    result = send("__decorated_#{method_name}", *args, &block)
    puts "[GENERIC DECORATOR] Después de #{method_name} en #{self.class}."
    result
  end
  super # Llama al method_added original si existe
end

end end

class ServicioDecorado def saludar(name) "Hola, #{name}!" end

def despedirse "Adiós!" end end

ServicioDecorado.prepend GenericMethodDecorator

s = ServicioDecorado.new puts s.saludar("Mundo") puts s.despedirse

Salida:

[GENERIC DECORATOR] Antes de saludar en ServicioDecorado.

Hola, Mundo!

[GENERIC DECORATOR] Después de saludar en ServicioDecorado.

[GENERIC DECORATOR] Antes de despedirse en ServicioDecorado.

Adiós!

[GENERIC DECORATOR] Después de despedirse en ServicioDecorado.

</details>

<div class="callout warning">⚠️ <strong>Advertencia:</strong> El uso de `method_added` es una técnica avanzada y puede llevar a comportamientos inesperados si no se maneja con extremo cuidado. Es fácil caer en recursiones infinitas o decorar métodos que no se deberían. Úsalo solo si comprendes completamente sus implicaciones.</div>

### Impacto en la Rendimiento

Aunque `prepend` es eficiente, añadir capas adicionales de lógica siempre tiene un pequeño overhead. Para aplicaciones de alta performance o métodos que se llaman millones de veces por segundo, este overhead podría ser significativo. Mide y profilea siempre que el rendimiento sea crítico.

### Depuración y Traceo

Depurar código con `prepend` puede ser un poco más complicado, ya que la ejecución de un método puede saltar entre el módulo prepended y la clase original. Las herramientas de depuración de Ruby suelen mostrar la pila de llamadas correctamente, pero ten en cuenta la cadena de ancestros modificada.

<div class="callout important">🔥 <strong>Importante:</strong> La claridad y la simplicidad son clave. Si un decorador se vuelve demasiado complejo o si la lógica de envoltura se entrelaza demasiado con la lógica del método original, podría ser una señal de que necesitas refactorizar o encontrar una solución de diseño diferente.</div>

--- 

## ✅ Casos de Uso Comunes para `prepend`

Aquí tienes un resumen de dónde `prepend` brilla como técnica de decoración:

*   **Extender librerías de terceros:** Modificar el comportamiento de clases de gemas sin forkar o modificar la gema directamente.
*   **Monitoreo y Métricas:** Instrumentar métodos para enviar datos a sistemas de monitoreo (ej. Datadog, New Relic).
*   **Transacciones de base de datos:** Envolver métodos en bloques de transacción.
*   **Seguridad:** Añadir controles de acceso basados en roles o permisos a métodos específicos.
*   **A/B Testing:** Modificar el comportamiento de un método para probar diferentes versiones.

<div class="timeline">
<div class="timeline-item"><strong>Paso 1: Identificar el método a decorar.</strong> ¿Cuál es el comportamiento que quieres extender o modificar?</div>
<div class="timeline-item"><strong>Paso 2: Crear un módulo.</strong> Define el método con el mismo nombre en este módulo.</div>
<div class="timeline-item"><strong>Paso 3: Implementar la lógica del decorador.</strong> Usa `super` para invocar el método original.</div>
<div class="timeline-item"><strong>Paso 4: Anteponer el módulo a la clase.</strong> Usa `ClaseObjetivo.prepend TuModuloDecorador`.</div>
<div class="timeline-item"><strong>Paso 5: Probar y refinar.</strong> Asegúrate de que el decorador funciona como esperas y no introduce efectos secundarios no deseados.</div>
</div>

--- 

## 🔮 Conclusión

`Module#prepend` es una característica sofisticada de Ruby que ofrece una forma elegante y potente de implementar decoradores de métodos. Permite extender y modificar el comportamiento de clases existentes de manera no intrusiva, mejorando la modularidad, la mantenibilidad y la testabilidad de tu código. Al comprender cómo `prepend` altera la cadena de ancestros y cómo usar `super` de forma efectiva, puedes desbloquear un nuevo nivel de flexibilidad en tus aplicaciones Ruby.

Recuerda usar esta herramienta con sabiduría, priorizando la claridad y la simplicidad en tus diseños. Cuando se aplica correctamente, los decoradores con `prepend` se convierten en un activo invaluable en tu caja de herramientas de desarrollo Ruby.

¡Experimenta, decora y desata el potencial de tu código Ruby!

Tutoriales relacionados

Comentarios (0)

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