Letture

 

Decorator Pattern con Ruby

Pubblicato translation missing: it.datetime.distance_in_words.almost_x_years fa da Andrea

Decorator è un design pattern comune nella programmazione ad oggetti che mi capita di usare spesso, almeno nelle sue forme più semplici.

L’utilizzo del pattern Decorator così come codificato nel libro Design Patterns della GoF è consigliato quando:

  • si vuole modificare le funzionalità di un dato oggetto in modo dinamico, senza per questo coinvolgere altri oggetti della stessa classe
  • si desidera che le funzionalità aggiunte possano essere eventualmente tolte
  • non è possibile utilizzare l’ereditarietà a causa dell’alto numbero di funzionalità aggiunte o a causa dell’elevato numero di sottoclassi che si renderebbero necessarie per supportare ogni loro possibile combinazione

Ecco lo schema UML che descrive il Decorator:

Data una classe astratta Component si crei una sottoclasse ConcreteComponent che implementi il metodo operation(). Si introduca ora una ulteriore sottoclasse astratta Decorator in cui il metodo operation() deleghi ad un oggetto di tipo ConcreteComponent. Le sottoclassi concrete di Decorator ConcreteDecoratorA e ConcreteDecoratorB si occuperanno quindi di aggiungere le funzionalità richieste all’oggetto originale.

Vediamo subito un esempio concreto, con del codice che ricalchi pedissequamente lo schema UML originale. Iniziamo col definire la classe astratta Logger e la classe concreta ConcreteLogger:

class Logger # Component
  def initialize
    raise 'This is an abstract class!'
  end
  
  def log(message) # operation()
    @buffer.puts message
  end
  
  def read
    @buffer.rewind
    @buffer.read
  end
end

class ConcreteLogger < Logger # ConcreteComponent
  def initialize
    @buffer = StringIO.new
  end
end

A questo punto passiamo alla classe astratta LoggerDecorator e alle sue implementazioni concrete:

class DecoratorLogger < Logger  # Decorator
  def initialize
    raise 'This is an abstract class!'
  end

  def log(message) # operation()
    @logger.log(message)
  end
  
  def read
    @logger.read
  end
end

class LineNumberedLogger < DecoratorLogger # ConcreteDecoratorA
  def initialize(logger)
    @logger = logger
    @count = 0
  end
  
  def log(message) # operation()
    @count += 1
    @logger.log("#{@count} #{message}")
  end
end

class TimeStampedLogger < DecoratorLogger #ConcreteDecoratorB
  def initialize(logger)
    @logger = logger
  end
  
  def log(message) # operation()
    @logger.log("#{Time.now} #{message}")
  end
end

Come già detto sopra è importante notare che il pattern Decorator fa uso della delegation.
A questo punto proviamo il codice creando qualche oggetto:

logger = ConcreteLogger.new
logger.log "hello"
puts logger.read
# hello

numbered = LineNumberedLogger.new(logger)
numbered.log "ciao"
puts numbered.read
# hello
# 1 ciao

timestamped = TimeStampedLogger.new(numbered)
timestamped.log "hola"
puts timestamped.read
# hello
# 1 ciao
# 2 Thu Jul 01 19:18:34 +0200 2010 hola

Una volta compreso il funzionamento del pattern originale è possibile (anzi doveroso) semplificare il codice come segue:

class Logger
  def initialize
    @buffer = StringIO.new
  end
  
  def log(message)
    @buffer.puts message
  end
  
  def read
    @buffer.rewind
    @buffer.read
  end
end

module Loggable
  def initialize(logger)
    @logger = logger
  end
  
  def log(message)
    @logger.log(message)
  end
  
  def read
    @logger.read
  end
end

class LineNumberedLogger
  include Loggable
  
  def log(message)
    @count ||= 1
    @logger.log("#{@count} #{message}")
    @count += 1
  end
end

class TimeStampedLogger
  include Loggable
  
  def log(message)
    @logger.log("#{Time.now} #{message}")
  end
end

Nei casi più semplici tutto questo non è necessario: Ruby infatti ci permette di modificare direttamente il comportamento di un singolo oggetto accedendo alla sua singleton class:

class Logger
  # as above
end

logger = Logger.new

class << logger
  alias log_old log
  
  def log(message)
    @count ||= 1
    log_old "#{@count} #{message}"
    @count += 1
  end
end

logger.log "Hello World"
puts logger.read
# 1 Hello World

class << logger
  alias log_old_1 log
  
  def log(message)
    log_old_1 "#{Time.now} #{message}"
  end
end

logger.log "Ciao Mondo"
puts logger.read
# 1 Hello World
# 2 Thu Jul 01 19:18:34 +0200 2010 Ciao Mondo

Nel primo esempio la nuova implementazione del metodo log aggiunge un timestamp alla stringa e poi richiama il metodo log_old che contiene l’implementazione originale. Nel secondo esempio l’aliasing del metodo viene fatta su log_old_1 per evitare di sovrascrivere il metodo log_old di cui abbiamo ancora bisogno.
Lo svantaggio di questo sistema è che diventa difficile gestire la catena di alias qualora si dovessero aggiungere parecchi altri decorator.

Un’altra soluzione potrebbe essere l’utilizzo di moduli per modificare il comportamento dell’oggetto logger:

class Logger
  # as above
end

module TimeStampable
  def log(message)
    super "#{Time.now} #{message}"
  end
end

module LineNumerable
  def log(message)
    @count ||= 1
    super "#{@count} #{message}"
    @count += 1
  end
end

logger = Logger.new
logger.extend LineNumerable

logger.log "Hello World"
puts logger.read
# 1 Hello World

logger.extend TimeStampable
logger.log "Ciao Mondo"
puts logger.read
# 1 Hello World
# 2 Thu Jul 01 19:18:34 +0200 2010 Ciao Mondo

Anche in questo caso abbiamo degli svantaggi: non è più possibile chiamare la vecchia implementazione di log se non con super nel blocco di codice che ridefinisce il metodo stesso, ma in molti casi questo potrebbe non essere un problema.

Archiviato in Ruby, Design Patterns