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

