Nessun risultato. Prova con un altro termine.
Guide
Notizie
Software
Tutorial

Un semplice plugin 'act as...' per Ruby on Rails

Trasformare il funzionamento di alcuni Moldel per farli "agire come" altri oggetti
Trasformare il funzionamento di alcuni Moldel per farli "agire come" altri oggetti
Link copiato negli appunti

Se esploriamo Rails Lodge, uno dei più famosi repository di plugins per Rails, noteremo che una buona percentuale delle estensioni presentate hanno un titolo del tipo: 'Acts As ...'.

I primi due esempi di questo strano e originale "behavior" sono da ricercarsi in 'Acts As List' e 'Acts As Tree', all'epoca non propriamente plugins (ora lo sono diventati) ma parte integrante del core di Rails.

Il concetto base che sottende tutti questi prodotti, suggerito proprio dalla particella 'Acts As', (cioè 'Agisci Come'), è quello di 'trasformare' il funzionamento di uno (o alcuni) models della nostra applicazione facendoli agire come liste (nel caso di Acts As List), come valute (Acts As Currency), o come un qualiasi altro comportamento voluto, attraverso l'utilizzo di una 'macro' che deve essere inserita all'interno dell'oggetto interessato come nell'esempio sottostante:

class Todo < ActiveRecord::Base
  acts_as_list
end

Nella pratica acts_as_list è una funzione che il plugin inietta (attraverso il suo init.rb) all'interno della classe ActiveRecord::Base rendendola di fatto disponibile a tutti i models.

Tale funzione ha il solo scopo di rendere disponibili, alla classe che la implementa, tutta una serie di metodi caratteristici di un particolare oggetto (ad esempio acts_as_list aggiunge metodi quali higher_item, move_lower, etc.) nonché di modificare alcuni dei comportamenti classici di un model (ad esempio Acts As Paranoid sovrascrive il metodo destroy della classe che lo implementa impedendo il cancellamento fisico di ogni suo record).

La chiave di questo funzionamento risiede nell'utilizzo sapiente dei Mixins, oggetto della prima sezione di questo articolo.

I Mixin

Matzumoto, creatore di Ruby, definisce i Mixins nel seguente modo: 'single inheritance with implementation sharing'. Nella pratica un Mixin è rappresentato semplicemente da un modulo che viene utilizzato come contenitore di metodi e successivamente 'mixxato' ad una o più classi in modo da rendere tali funzioni a loro disponibili.

La definizione di Matz riesce quindi a cogliere un aspetto essenziale di questa struttura; pur restando nel campo dell'ereditarietà singola utilizzando i Mixins è possibile condividere implementazioni tra più classi (simulando in questo modo alcuni aspetti dell'ereditarietà multipla).

module Gasolio
def add_to_serbatoio(litri) [...] end 
def get_stato_serbatoio [...] end
end

module Ruota
def get_momento_angolare [...] end
end

class Bicicletta
  include Ruota
end

class Aereo
  include Gasolio
end

class Macchina
  include Gasolio
  include Ruota
end

In questo esempio i moduli Ruota e Gasolio contengono un set di istruzioni specifiche all'aspetto di cui trattano e ognuna delle tre classi incorpora i moduli che sono propedeutici al proprio funzionamento. L'istruzione include rende le funzioni dichiarate all'interno del modulo richiesto disponibili a tutte le istanze dell'oggetto richiedente, quindi in questo caso potremmo eseguire:

mountain_bike = Bicicletta.new
mountain_bike.get_momento_angolare 

Quanto appena enunciato evidenzia come la definizione di Matz sia perfettamente aderente al concetto di Mixin che si dimostra essere un'ottima alternativa (anche se più limitata) all'ereditarietà multipla. Inoltre si cominciano ad intravedere delle somiglianze tra il funzionamento di questa implementazione e quello di un 'Acts_as' plugin; infatti l'istruzione include Gasolio emula in parte lo stesso funzionamento delle 'acts_as' macro viste in precedenza.

Prima di passare alla sezione successiva è bene ricordare che oltre all'istruzione include esiste un'istruzione chiamata extend che aggiunge le funzioni presenti nel modulo come metodi di classe anziché di istanza.

Acts as Timed

Ora abbiamo tutti gli strumenti necessari per creare il nostro primo 'Acts_as' plugin, per dotare l'esempio di una qualche connotazione funzionale il plugin aggiungerà due metodi ad ogni model che lo invochi; il primo (give_me_the_time), di classe, ritornerà la data e l'ora corrente mentre il secondo (is_this_record_very_old?), di istanza, ritornerà true se la colonna created_at del record corrente è più vecchia di 5 giorni.

Ecco una lista step-by-step di ciò che stiamo per implementare:

  • un modulo ActsAsTimed che dovrà contenere tutta la logica del plugin
  • una funzione acts_as_timed che dovrà funzionare da 'appiglio' (hook) per tutti i models che desiderino utilizzare le funzionalità di questo plugin
  • un sotto-modulo con i metodi di classe che dovremo fornire alla classe che implementa questo plugin
  • un sotto-modulo simile al precedente ma con i metodi di istanza
  • un'istruzione per agganciare ActsAsTimed ad ActiveRecord::Base

Per prima cosa creiamo un'applicazione Rails

rails timed_sample
cd timed_sample

Ora creiamo la struttura che conterrà il nostro plugin, per farlo utilizziamo uno dei 'generators' che Rails ci mette a disposizione:

ruby script/generate plugin acts_as_timed
cd vendor/plugins/acts_as_timed/

Bene, ora concentriamoci sull'unico file presente nella cartella 'lib': acts_as_timed.rb. Questo file conterrà tutta lo logica applicativa del plugin che stiamo creando, apriamolo quindi con un editor di testi e scriviamo il seguente codice:

module ActsAsTimed
  def self.included(base)
    base.extend(BaseMethods)
  end

  module BaseMethods
    def acts_as_timed
      self.send :include,InstanceMethods
      self.send :extend,ClassMethods
    end
  end
  
  module ClassMethods
  end
  
  module InstanceMethods 
  end
end

Analizziamo quanto appena scritto: la prima funzione self.included(base) viene chiamata automaticamente ogniqualvolta ActsAsTimed venga incluso all'interno di una classe (con la variabile base contenente la classe che ha invocato l'inclusione); all'interno di questo metodo c'è un'unica riga di codice base.extend(BaseMethods) che, come visto in precedenza, aggiunge alla classe contenuta nella variabile base i metodi del sotto-modulo BaseMethods sotto forma di funzioni di classe (perché è stato usato extend e non include).

Quindi se a questo punto si dovesse includere ActsAsTimed in ActiveRecord::Base dovremmo trovare nell'elenco dei suoi metodi anche acts_as_timed, proviamo:

ruby script/console
>> ActiveRecord::Base.send :include, ActsAsTimed
=> #...
>> ActiveRecord::Base.methods.include?("acts_as_timed")
=> true

Perfetto! A questo punto possiamo proseguire ed analizzare cosa succede all'interno del metodo acts_as_timed. Le due righe di codice presenti utilizzano la stessa funzione send dell'esempio appena illustrato, tale funzione ha l'unico scopo di invocare l'azione passata come primo parametro inviando a quest'ultima i parametri dal secondo in poi. Quindi il tutto potrebbe essere tradotto in:

self.include InstanceMethods
self.extend ClassMethods

purtroppo però include è un metodo privato e non può essere lanciato a meno di non trovarsi all'interno della classe stessa; send serve solamente ad aggirare questo problema in quanto dà la possibilità di invocare qualsiasi metodo all'interno di una classe (privato o non). Lo scopo delle due righe di codice appena trattate dovrebbe essere oramai abbastanza chiaro: la prima include nella classe che ha invocato acts_as_timed tutte le funzioni presenti nel sotto-modulo InstanceMethods come metodi di istanza, la seconda fa la stessa cosa con il modulo ClassMethods utilizzando però la funzione extend (quindi i metodi all'interno di ClassMethods verranno resi disponibili come metodi di classe).

Non ci resta ora che popolare questi due sotto-moduli con i metodi give_me_the_time e is_this_record_very_old?; ecco come si dovrebbe presentare il file completo:

module ActsAsTimed
  def self.included(base)
    base.extend(BaseMethods)
  end
  
  module BaseMethods
    def acts_as_timed
      self.send :include,InstanceMethods
      self.send :extend,ClassMethods
    end
  end
  
  module ClassMethods
    def give_me_the_time
      Time.new 
    end
  end
  
  module InstanceMethods 
    def is_this_record_very_old?
    
      raise "Column created_at not found" unless 						
        self.class.columns_hash.keys.include?("created_at")
		  raise "Column created_at not initialized" if 
  		  created_at.nil?

      created_at < Time.new - 5.days
    end
  end
end

Adesso dobbiamo solo fare in modo che ActsAsTimed venga incluso automaticamente all'interno di ActiveRecord::Base, per farlo dobbiamo aggiungere la seguente istruzione nel file init.rb che si trova nella cartella principale del plugin:

ActiveRecord::Base.send :include, ActsAsTimed

Infine testiamo quanto abbiamo realizzato (per farlo dobbiamo prima creare un model):

ruby script/generate model order name:string
rake db:create
rake db:migrate

Ora inseriamo la macro acts_as_timed all'interno di Order, per farlo editiamo come segue il file app/models/order.rb:

class Order < ActiveRecord::Base
  acts_as_timed
end

Non ci resta che eseguire la console della nostra applicazione per verificare il buon funzionamento del plugin:

ruby script/console
>> Order.give_me_the_time
=> Sun Jan 25 23:12:26 +0000 2009
>> Order.create(:name=>'hello').is_this_record_very_old?
=> false

Conclusioni

Recentemente si sta ricominciando a parlare con interesse di Aspect Oriented Programming, cioè di quel paradigma di programmazione che tende ad associare ad ogni oggetto del sistema un certo numero di aspetti, dai quali questo oggetto è condizionato e dai quali riceve nuove e diverse funzionalità (ad esempio alcuni oggetti del sistema potrebbero adottare l'aspetto 'logging' e quindi loggare ogni chiamata ai propri metodi); in un certo senso questa classe di plugins avvicina Ruby a questo paradigma di programmazione, infatti se pensiamo che ognuno di questi plugin fa in modo che l'oggetto che lo implementa 'si comporti come ...' risultano evidenti alcune somiglianze tra questo comportamento e gli 'aspetti' dell'aspect oriented programming (l'aspetto logging appena accennato trova infatti la sua controparte in Acts As Scribe).

Ti consigliamo anche