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!