Letture

 

actionmailer-callbacks per rails3: concept di una gemma

Pubblicato circa un anno fa da Andrea

Tempo fa ho sviluppato per rails 2 la gemma actionmailer-callbacks che permetteva di eseguire codice arbitrario prima e dopo la creazione o l’invio di una email, un po’ come siamo da sempre abituati a fare con i controller rails utlizzando le macro before_filter e after_filter o nei modelli con le callback di active record.

Con l’arrivo di rails 3 action mailer è stato riscritto aggiungendo nuove funzionalità, ora i nuovi observer e interceptor ci permettono di chiamare dei metodi rispettivamente dopo la creazione e dopo l’invio dell’email: per la versione compatibile con rails 3 ci resta solo da implementare le funzionalità legate al momento precedente alla creazione dell’email. Per prima cosa definiamo l’interfaccia che vorremmo utlizzare nelle varie classi mailer (ricordiamoci che l’interfaccia viene prima di tutto):

class MyAppMailer < ApplicationMailer
  before_create :log_args

  def greet_new_user(user)
    #...
  end

  private

  def log_args(*args)
    Rails.logger.info "A #{self.class} was initialized with #{args.inspect}"
  end
end

Il codice precedente deve permetterci quindi di eseguire log_args prima del processo normale di costruzione dell’email. Modifichiamo ActionMailer aggiungendo la definizione di before_create:

class ActionMailer::Base
  class << self
    def before_create(*methods)
      include CallbackProcessor
      add_callbacks(*methods)
    end

    private

    def add_callbacks(*methods)
      methods.each do |method|
        before_create_callbacks << method
      end
    end
  end
end

add_callbacks si occupa di includere il modulo CallbackProcessor nella classe su cui viene chiamato e di aggiungere alle liste delle callback i nomi dei vari metodi che intendiamo usare (il modulo CallbackProcessor va creato prima di modificare ActionMailer nonostante in questo post ne mostri il codice solo in seguito).

Infine creiamo il modulo mancante definendo una serie di metodi che estendono la classe del mailer non appena viene chiamato before_create_callbacks:

module CallbackProcessor
  def self.included(base)
    base.extend ClassMethods
  end

  module ClassMethods
    def before_create_callbacks
      @before_create_callbacks ||= []
    end
  end

  def initialize(*args)
    self.class.before_create_callbacks.each do |callback|
      send callback, *args
    end
    super
  end
end

A questo punto ogni volta che verrà creata/inviata una email con

MyAppMailer.greet_new_user 

verrà eseguito il metodo log_args prima di tutto il resto.

L’implementazione reale della gemma è più complessa ma il meccanismo è sostanzialmente il medesimo.

In chiusura sottolineo una importante differenza tra le versioni rails 2 e rails 3 della mia gemma: nel primo caso utilizzai alias_method_chain per modificare direttamente il comportamento di ActionMailer::Base, mentre nella nuova versione mi sono affidato ad un sistema più pulito promosso da Yehuda Katz in un post di 3 anni fa ma ancora attualissimo.

Chi fosse interessato all’implementazione della gemma può trovarla
qui. (la versione rails 3 è al momento ancora WIP).

Archiviato in Ruby on Rails, Ruby

commenti

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

commenti

Observer design pattern con Ruby

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

Observer è uno dei design pattern che mi capita di usare più spesso scrivendo applicazioni con Ruby on Rails: quando si ha a che fare con interazioni tra modelli la cosa più sensata da fare è infatti ricorrere ad un observer di ActiveRecord.

Observer è un design pattern tanto frequente e utile che persino la Standard Library di Ruby include un modulo (chiamato non a caso Observable) che ci semplifica la vita ogni qual volta abbiamo bisogno di una classe che al verificarsi di determinate condizioni si occupi di informare altri oggetti.

Ho realizzato un semplice esempio che sfrutta il modulo Observable per realizzare un gioco simile al bingo; si inizia creando la classe che si occuperà di gestire il gioco informando i giocatori sui numeri estratti:

require 'observer'

class Bingo
  include Observable
  
  NUMBERS = (1..90).to_a
  
  def initialize
    @remaining_numbers = NUMBERS.sort_by {rand}
    @players = []
  end
  
  def start
    extract until @remaining_numbers.empty?
  end
  
  def extract
    extracted = @remaining_numbers.pop
    changed
    notify_observers(extracted)
  end
end

I passi da seguire qui sono:

  • caricare e includere il modulo Observable nella classe
  • creare un array (in questo caso @players) che contenga gli oggetti da tenere informati
  • ogni qual volta si vuole inviare una notifica si deve usare il metodo changed per dichiarare che c’è stato un cambiamento e successivamente chiamare il metodo notify_observers

Procediamo ora con la creazione della classe Player che si occuperà di ricevere le notifiche in arrivo e agire di conseguenza:

class Player
  def initialize(name, numbers)
    @name = name
    @numbers = numbers
  end
  
  def update(extracted)
    @numbers.delete(extracted)
    call_for_victory if @numbers.empty?
  end

  def call_for_victory
    puts "Bingo! #{@name} won this game."
    exit
  end
end

Qui l’unico passo richiesto è creare il metodo update che verrà chiamato ad ogni notifica.
A questo punto non ci resta che creare qualche instanza delle classi e iniziare il gioco, che si concluderà con la vittoria del primo giocatore che avrà visto chiamare tutti i suoi numeri:

game = Bingo.new
fred = Player.new('Fred', [39, 65, 66, 19, 5, 51])
wilma = Player.new('Wilma', [8, 16, 66, 50, 90, 12])
barney = Player.new('Barney', [3, 10, 40, 22, 67, 34])

[fred, wilma, barney].each do |player|
  game.add_observer player
end

game.start

Archiviato in Ruby, Design Patterns

commenti

Database as a fortress

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

Qualche giorno fa durante dei controlli sul database di una applicazione che avevo contribuito a scrivere abbiamo casualmente trovato un paio di record non validi.
La cosa mi ha lasciato abbastanza sorpreso, visto che il codice applicativo era stato scritto in modo da impedire la creazione di questo genere di record invalidi (dati duplicati):

class Referral < ActiveRecord::Base
  belongs_to :travel_plan
  # ...
  validates_uniqueness_of :url, :scope => :travel_plan_id
end

A quanto pare questo non è stato sufficiente. Poco male, sia perché il fenomeno è stato molto limitato (due casi su oltre 200 mila record), sia perché esiste un rimedio molto semplice per questo tipo di problema: inserire delle constraint direttamente al livello del database.
Ruby on Rails rende molto semplice questo tipo di operazione attraverso l’uso di una migration:

class RemoveDuplicatedReferrals < ActiveRecord::Migration
  # ...
  add_index :referrals, [:url, :travel_plan_id], :unique => true
end

A questo punto mi sento piuttosto sicuro che il problema non si ripresenterà in futuro.
L’ultimo step necessario per considerare la faccenda definitivamente risolta è gestire l’eccezione generata all’interno dell’applicazione in caso di insert fallito a livello del database:

class TravelPlan < ActiveRecord::Base
  has_many :referrals
  # ...
  def add_referral_vote(referer)
    referrals.create(:url => referer) 
  rescue ActiveRecord::StatementInvalid
    false
  end
end

La lezione che possiamo trarre da questo caso è che se abbiamo veramente a cuore la validità dei dati salvati nel nostro database non dovremmo accontentarci dei controlli a livello applicativo.

I dati sono spesso il vero patrimonio di una azienda, soprattutto se sono stati raccolti in molti anni di lavoro, pertanto è buona cosa salvaguardarli nel migliore dei modi possibile, anche a costo di perdere un po’ di tempo rinforzando il database dall’interno con ulteriori controlli di validità, univocità eccetera.
Una scelta di questo genere potrebbe rallentare il processo di sviluppo dell’applicazione, soprattutto durante la fase iniziale di prototipizzazione. E’ per questo che in genere preferisco effetturare queste modifiche quando veramente serve, ovvero una volta che l’applicazione ha raggiunto una forma piuttosto stabile o è pronta per andare in produzione.

Archiviato in debugging, Ruby on Rails

commenti