Programando en Ruby

La Guía de los "Programadores Pragmáticos"

Object-Oriented Design Libraries



Una de las cosas interesantes de Ruby es el modo en que realiza la distinción entre diseño e implementación. Ideas que deben expresarse a nivel de diseño en otros lenguajes pueden implementarse directamente en Ruby.

Para ayudar en este proceso, Ruby da sopote a algunas estrategias a nivel de diseño.

Normalmente, estas cuatro estrategias requerirían código explícito para cada vez que se implementasen. Con Ruby, pueden ser abstraídas en una librería y reutilizadas libremente y de forma transparente.

Antes de entrar de lleno en las descripciones de las librerías propiamente dichas, echemos un vistazo a la estrategia claramente más simple.

El patrón Visitante

Es el método each.

Library: delegate

La delegación de objetos es una manera de componer objetos---extender un objeto con las capacidades de otro---en tiempo de ejecución. Ésto facilita el escribir código flexible y sin acoplamiento, ya que no existen dependencias en tiempo de compilación entre los usarios de las clases generales y las delegadas.

La clase Ruby Delegator implementa un sencillo pero potente esquema de delegación, donde las solicitudes se pasan automáticamente desde una clase maestra a una de sus delegadas o sus ancestros, y donde la delegación puede ser modificada en tiempo de ejecución con una simple llamada a un método.

La librería delegate.rb proporciona dos mecanismos para permitir a un objeto enrutar mensajes a un delegado.

  1. Para casos simples donde la clase del delegado es fija, haz que tu clase maestra sea una subclase de DelegateClass, pasándole el nombre de la clase delegada como parámetro (Ejemplo 1). Después, en el método initialize de tu clase, llamarías a su superclase, pasándole el objeto que debe ser delegado. Por ejemplo, para declarar una clase Fred que también soporte todos los métodos en Flintstone, escribirías

    class Fred < DelegateClass(Flintstone)
      def initialize
        # ...
        super(Flintstone.new(...))
      end
      # ...
     end
    
    Existe una una sutil diferencia respecto a usar subclases. Con subclases, solo existe un objeto, el cual tiene los métodos y la clase definida, su padre, y sus ancestros. Con delegación existen dos objetos enlazados de tal modo que llamadas a uno pueden ser delegadas a otro.

  2. Para los casos en los que la delegación necesite ser más dinámica, haz que la clase maestra sea subclase de SimpleDelegator (Ejemplo 2). Puedes también añadir capacidades de delegación a un objeto existente usando SimpleDelegator (Ejemplo 3). En estos casos, puedes llamar al método __setobj__ en SimpleDelegator para cambiar el objeto delegado en tiempo de ejecución.

Ejemplo 1. Utiliza el método DelegateClass y haz una subclase del valor devuelto cuando necesites una clase con su propio comportamiento que también delega a un objeto de otra clase. En este ejemplo, asumimos que el array @sizeInInches es grande, por lo que queremos que solo exista una copia de él. Después definimos una clase que accede a él convirtiendo los valores a pies.

require 'delegate'

sizeInInches = [ 10, 15, 22, 120 ]

class Feet < DelegateClass(Array)   def initialize(arr)     super(arr)   end   def [](*n)     val = super(*n)     case val.type     when Numeric; val/12.0     else;         val.collect {|i| i/12.0}     end   end end

sizeInFeet = Feet.new(sizeInInches)
sizeInInches[0..3] » [10, 15, 22, 120]
sizeInFeet[0..3] » [0.8333333333, 1.25, 1.833333333, 10.0]

Ejemplo 2. Utiliza una subclase de SimpleDelegator cuando necesites un objeto que tiene su propio comportamiento y delega a diferentes objectos durante su tiempo de vida. Éste es un ejemplo del patrón Estado. Los objetos de la calse TicketOffice venden tickets si hay un vendedor disponible, o te dicen que vuelvas mañana si no lo hay.

require 'delegate'

class TicketSeller   def sellTicket()     return 'Here is a ticket'   end end

class NoTicketSeller   def sellTicket()     "Sorry-come back tomorrow"    end end

class TicketOffice < SimpleDelegator   def initialize     @seller = TicketSeller.new     @noseller = NoTicketSeller.new     super(@seller)   end   def allowSales(allow = true)     __setobj__(allow ? @seller : @noseller)     allow   end end

to = TicketOffice.new
to.sellTicket » "Here is a ticket"
to.allowSales(false) » false
to.sellTicket » "Sorry-come back tomorrow"
to.allowSales(true) » true
to.sellTicket » "Here is a ticket"

Ejemplo 3. Crea objetos SimpleDelegator cuando necesites un solo objeto que delega todos sus métodos a dos o más objetos diferentes.

# Example 3 - delegate from existing object
seller   = TicketSeller.new
noseller = NoTicketSeller.new
to = SimpleDelegator.new(seller)
to.sellTicket » "Here's a ticket"
to.sellTicket » "Here's a ticket"
to.__setobj__(noseller)
to.sellTicket » "Sorry-come back tomorrow"
to.__setobj__(seller)
to.sellTicket » "Here's a ticket"

Library: observer

El patrón Observador, también conocido como Publicar/Suscribir, proporciona un sencillo mecanismo para que un objeto informe a un conjunto de objetos interesados cuando su estado cambia.

En la implementación Ruby, la clase notificadora añade el módulo Observable, el cual proporciona los métodos para gestionar los objetos observadores asociados.

add_observer(obj) Añade obj como observador de este objeto. obj recibirá las notificaciones.
delete_observer(obj) Elimina obj como observador de este objeto. Ya no recibirá más notificaciones.
delete_observers Elimina todos los observadores asociados con este objeto.
count_observers Devuelve el número de observadores asociados con este objeto.
changed(nuevoEstado=true) Establece el estado de modificado del objeto. Se enviarán las notificaciones solamente si el estado de modificado es true.
changed? Comprueba el estado de modificado del objeto.
notify_observers(*args) Si el estado de modificado del objeto es true, invoca el método update en cada uno de los observadores asociados, pasándoles los argumentos dados. El estado de modificado se establece a continuación a false.

Los observadores deben implementar el método update para recibir las notificaciones.

require "observer"

  class Ticker # Periodically fetch a stock price     include Observable

    def initialize(symbol)       @symbol = symbol     end

    def run       lastPrice = nil       loop do         price = Price.fetch(@symbol)         print "Current price: #{price}\n"         if price != lastPrice           changed                 # notify observers           lastPrice = price           notify_observers(Time.now, price)         end       end     end   end

  class Warner     def initialize(ticker, limit)       @limit = limit       ticker.add_observer(self)   # all warners are observers     end   end

  class WarnLow < Warner     def update(time, price)       # callback for observer       if price < @limit         print "--- #{time.to_s}: Price below #@limit: #{price}\n"       end     end   end

  class WarnHigh < Warner     def update(time, price)       # callback for observer       if price > @limit         print "+++ #{time.to_s}: Price above #@limit: #{price}\n"       end     end   end

ticker = Ticker.new("MSFT") WarnLow.new(ticker, 80) WarnHigh.new(ticker, 120) ticker.run
produces:
Current price: 83
Current price: 75
--- Sun Jun 09 00:10:25 CDT 2002: Price below 80: 75
Current price: 90
Current price: 134
+++ Sun Jun 09 00:10:25 CDT 2002: Price above 120: 134
Current price: 134
Current price: 112
Current price: 79
--- Sun Jun 09 00:10:25 CDT 2002: Price below 80: 79

Library: singleton

El patrón de diseño Singleton garantiza que solo puede crearse una instancia de una clase en particular.

La librería singleton hace que ésto sea sencillo de implementar. Añade el módulo Singleton dentro de cada clase que debe ser singleton, y el método new de la clase se hará privado. En su lugar, los usuarios de la clase llamarán al método instance, el cual devuelve una instancia singleton de esa clase.

En este ejmplo, las dos instancias de MyClass son el mismo objeto.

require 'singleton'
class MyClass
  include Singleton
end
a = MyClass.instance » #<MyClass:0x401b4ca8>
b = MyClass.instance » #<MyClass:0x401b4ca8>


Extraído del libro "Programming Ruby - The Pragmatic Programmer's Guide"
Copyright © 2000 Addison Wesley Longman, Inc. liberado bajo los términos de la Open Publication License V1.0.
La referencia está disponible en: download.