Classi senza Burocrazia

Posted by Chiaroscuro, Mon Nov 05 01:51:00 UTC 2007

Adoro Ruby, e poichè lo adoro, non riesco proprio a lasciarlo in pace. Dopo averci lavorato per un po di tempo ho iniziato a diventare insofferente nei confronti di alcune attività base di definizione di classe. Quante volte devo rielencare tutti gli attributi di classe in attr_accessor, come parametri di initialize e nel corpo del costruttore?

Tediato da questa burocrazia ho sfoderato un po di metaprogrammazione e ruby dinamico.

Prendiamo un caso base e procediamo ad astrarlo un passo alla volta. Partiamo con una classe Cat, il mio animale preferito. Come è noto un gatto può essere univocamente e universalmente identificato in base a nome, sesso e colore:

1
2
3
4
5
6
7
  class Cat
    attr_reader :name, :gender, :colour

    def initialize name, gender, colour
      @name, @gender, @colour = name, gender, colour
    end
  end    

Perdonatemi l’idioma per l’assegnamento degli attributi di classe, ma è molto rapido da utilizzare con il copy and paste.

Il primo passo consisterà nel sostituire initialize con qualcosa di più conciso e immediato. Idealmente vorremmo poter usare questo comando nel corpo della classe per poter inizializzare gli attributi:


  init_with :name, :gender, :colour

Il lettore più attento mi dirà che a questo scopo esiste già Struct che mi permette di scrivere concisamente:


  Cat = Struct.new :name, :gender, :colour

E’ vero, Struct è molto utile e rapido, ma presenta anche alcuni problemi. Il problema principale per me consisteva nella difficoltà di trovare al volo classi definite con Struct all’interno del codice. L’occhio è abituato a cercare una struttura di classe, con un certo tipo di indentazione e syntax-colouring e una classe definita con Struct tende a sfuggire e a confondersi con il resto del codice. Un altro problema, a volte molto fastidioso, è che Struct non rappresenta le sue proprietà come attributi di classi, quelli con la chiocciolina per intenderci, ma probabilmente con una hash table. Questo spesso confonde le nostre aspettative su certe operazioni possibili.

Affrontiamo ora il problema di definire init_with. Questo metodo, per poter essere disponibile nel corpo di una qualunque classe, deve essere definito come metodo di classe in Object, oppure nella classe Module, che lo rende disponibile a tutte le classi e tutti i moduli. Riapriamo la classe Module e definiamo un metodo che prende un numero variabile di argomenti:

1
2
3
4
5
6
class Module

    def init_with *args
    end
    
end

Il metodo init_with dovrà generare un costruttore initialize nel contesto della classe che invoca init_with:

1
2
3
4
5
6
7
8
9
10
  def init_with *args
    args_list = args.map {|e| e.to_s }
    attributes_list = args_list.map {|e| '@'+e }
        
    class_eval %{
      def initialize #{args_list.join ','}
        #{attributes_list.join ','} = #{args_list.join ','}
      end
    }
  end    

Innanzitutto generiamo una lista di attributi espressi come stringhe anzichè come simboli:


  args_list = args.map {|e| e.to_s }

Poi decoriamo ogni attributo con una chiocciolina:


  attributes_list = args_list.map {|e| '@'+e }

Infine valutiamo nel contesto della classe chiamante un template di initialize:

1
2
3
4
5
  class_eval %{
    def initialize #{args_list.join ','}
      #{attributes_list.join ','} = #{args_list.join ','}
    end
  }

E questo è quanto. Ora possiamo definire il nostro gatto con un semplice:

1
2
3
4
  class Cat
    attr_reader :name, :gender, :colour
    init_with   :name, :gender, :colour
  end    

mmmhh.. ancora troppe ripetizioni? Facciamo un ultimo sforzo:

1
2
3
4
5
6
7
8
9
10
11
  class Module
    def init_with_readers *args
      sym_list = args.map {|e| ":#{e}" }
      sym_list_string = sym_list.join ', '
        
      class_eval %{
        attr_reader #{sym_list_string} 
        init_with #{sym_list_string}
      }
    end    
  end

Questo codice genera sia i reader che il costruttore in un unico passo. Notate che abbiamo anche riutilizzato init_with senza dover ripetere il codice di generazione per il costruttore.

Il nostro gatto in ultima istanza può ora essere definito come:

1
2
3
  class Cat
    init_with_readers :name, :gender, :colour
  end    

Ruby non è fatto per essere lasciato in pace. Quando scorgete una opportunità per rendere il codice più chiaro e rimuovere ripetizioni, mettete mano alla metaprogrammazione!

Filed Under: Filosofia Linguaggio Metodologia | Tags:

Comments

  1. Luca Guidi 02.02.08 / 16PM
    Davvero un ottimo articolo, mi ero ripromesso di studiare una tecnica per eliminare le ripetizioni nell'initialize, ma mi hai preceduto. Complimenti.
  2. Luca Guidi 02.02.08 / 16PM
    Mi scuso per il flood, ma la pagina continuava a ritornarmi un errore.
  3. Carlo 02.02.08 / 16PM
    Interessante... molto interessante! Soprattutto perché mostra un esempio di come la metaprogrammazione possa essere utilizzata "semplicemente" per renderci la *vita più facile*. Ottimo lavoro Chiaro!
  4. Chiaro 02.02.08 / 16PM
    Luca, ci sono altre ripetizioni che ti danno fastidio? Magari possiamo scoprire come abbatterle :-)
  5. riffraff 02.02.08 / 16PM
    AFAICT non c'è bisogno di passare per una rappresentazione in forma di stringa, e quindi non serve usare Evil Eval:
    class Module
      def init_with(*names)
        define_method :initialize do |*args|
          names.each do |name|
            instance_variable_set("@#{name}",args.shift)
          end
        end
      end
    
      def init_with_readers(*names)
        attr_reader *names
        init_with *names
      end
    end
    
    C'è una differenza che potrebbe essere sostanziale, ed è dovuta al fatto che #arity restituirà un valore differente (non ho mai visto un uso utile & intelligente di #arity, ma non si sa mai). A parte questo.. bell articolo :)
  6. Chiaro 02.02.08 / 16PM
    Bella RiffRaff, la tua soluzione è molto più pulita! Inizialmente facevo così anch'io, poi mi sono abituato all'eval perchè in casi più complessi lo trovavo più leggibile al colpo d'occhio.. era + come fare templating che metaprogrammare e mi risultava più semplice. mi piace molto anche come con la doppia closure carpiata di args e names semplifichi il codice.
  7. LucaPette 02.02.08 / 16PM
    Ottimo. Molto interessante.

Have your say

A name is required. You may use HTML in your comments.