Ruby language

Le classi - 1                

Tutto in Ruby è un oggetto e ogni oggetto è istanza di una classe. Logico quindi che l'argomento di questo paragrafo sia estremamente importante. E' a livello di classe che vengono definiti i metodi che, in un certo senso, daranno vita all'oggetto. Come è noto un concetto fortemente legato alle classi è quello delle ereditarietà, ovvero la capacità di una classe di ereditare metodi da un'altra classe; Ruby, come la maggior parte dei linguaggi, supporta l'ereditarietà singola. Le classi possono essere manipolate solo attraverso i loro metodi ed è possibile definire degli attributi (getter e setter). Tuttavia, e questa è una caratteristica interessante, è possibile effettuare delle modifiche sulle classi che sono aperte all'aggiunta di metodi, ivi compresi quelli "singleton" (che permettono una sola istanziazione della classe). Le classi insomma in Ruby non sono mai chiuse. L'argomento è molto corposo e coinvolge anche altre entità, come i mixin che vedremo all'opera nei paragrafi di seguito. Ancora, le classi in Ruby sono "first class object", ovvero possiamo riassumere questo concetto dicendo che non vi sono limitazioni al loro uso.  Ricordandoci che il nome delle classi inizia con la maiuscola e quello dei metodi, di norma, con la minuscola vediamo qualche cosa di pratico.

la keyword che definisce la classi in Ruby è class.

class Persona
end

Bene, abbiamo creato la nostra prima classe in Ruby. Come si vede abbiamo anche la parolina end che termina la definizione della classe.

L'istanziazione di una classe avviene attraverso un'altra ben nota keyword: new.

p1 = Persona.new

p1 è una nuova istanza della classe Persona. Questa che abbiamo appena creato è una classe a tutti gli effetti ma non è molto ricca di contenuti. Come detto a dar vita alle ed a distinugerne i contenuti ci pensano i metodi e le istanze variabili (che illustreremo tra pochissimo) che attribuiamo ad esse. Le sue caratteristiche possono quindi essere ulteriormente specificate tramite una inizializzazione di queste chiamiamole entità interne. Ruby ci mette a disposizione il metodo inizialize che è un speciale nelle sue funzioni:

class Persona
  def initialize(nome, cognome)
  @nome, @cognome = nome, cognome
  end
end

initialize collabora strettamente con new; quest'ultimo infatti richiama immediatamente il metodo initialize il quale, ovviamente, è locale, non globale, e vale per la classe alla quale si riferisce. Ovviamente in fase di dichiarazione è necessario passare il numero corretto di parametri altrimenti incorreremmo in un errore del tipo:

in `initialize': wrong number of arguments (1 for 2) (ArgumentError)

questo ad esempio passando un solo parametro laddove ne erano richiesti due. Il metodo initialize è privato, quindi può essere usato in fase di istanziazione della classe ma non può essere successivamente richiamato in maniera esplicita. Di questo aspetto parleremo anche nel prossimo paragrafo. Un'altra cosa che non vi sarà sfuggita è la metodologia di manipolazione delle variabili; nome e cognome vengono attribuite a due istanze introdotte dal simbolo @ il quale fa effettivamente parte del nome. In pratica il nostro @nome non è la stringa "nome" al quale è stato appiccicato un qualche operatore come prefisso, si chiama proprio "@nome". Su queste istanze viene trasferita l'informazione che i parametri si portano dietro. Detto che in Ruby è il solito . (punto) il separatore tra le varie entità di una classe vediamo un primo esempio:

  Esempio 10.1
1
2
3
4
5
6
7
class Persona
def initialize(nome, cognome)
  @nome, @cognome = nome, cognome
  end
end

p = Persona.new("Mario", "Rossi")

p, definito alla riga 7, è una istanza della classe Persona definita dalla 1 alla 5; parleremo di queste istanza come di oggetto derivato da un classe. Le stringhe "Mario" e "Rossi" sono passate rispettivamente in nome e cognome. I valori memorizzati nelle variabili sono poi assegnati alle rispettive istanze. Non è possibile assegnare valori di default alle istanze variabili. Tanto meno è possibile accedere dall'esterno alle istanze variabili che sono manovrabili solo attraverso metodi interni alla classe a cui le istanze appartengono. Notiamo anche in questa occasione che definire un metodo all'interno di una classe non differisce praticamente in nulla rispetto a quanto già visto sui metodi.

Proseguendo nella costruzione di una classe possiamo proseguire seguendo direttamente le sitruzioni fornite da Matz in persone che, anche nel suo magnifico testo sul linguaggio, suggerisce di dotare ogni classe di un metodo che si chiama to_s, come fa l'autore nei suoi esempi, che offre una rappresentazione dell'oggetto; tale metodo è utile in fase di debug e può eventualmente essere eliminato successivamente.

  Esempio 10.2
1
2
3
4
5
6
7
8
9
10
11
class Persona
  def initialize(nome, cognome)
    @nome, @cognome = nome, cognome
  end
  def to_s
    "(#@nome, #@cognome)"
  end
end

p = Persona.new("Mario", "Rossi")
puts p

to_s è non è chiamato in causa in maniera accidentale e vedremo di cosa si tratta. Comunque per il momento basta questo esempio.

La cosa importante ora è quella di poter usare le entità definite all'interno della classe. Per fare questo dobbiamo definire un sistema per accedere alle istanze variabili. Ebbene per fare questo possiamo ricorrere a dei metodi di accesso; iniziamo da un getter ovvero un metodo che mette a disposizione l'istanza interna. L'esempio seguente ce lo presenta:

  Esempio 10.3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Persona
def initialize(nome, cognome)
@nome, @cognome = nome, cognome
end

def nome
@nome
end

def cognome
@cognome
end

end

p = Persona.new("Mario", "Rossi")
puts p.nome
puts p.cognome

Ed eccoli, evidenziati in rosso, i getter che rendono disponibili le due istanze variabili che vengono effettivamente esposte alle righe 17 e 18. E' da notare che, diversamente da quanto può sembrare e da quanto avviene in altri linguaggi, le istruzioni alle righe 17 e 18 non richiamano direttamente le istanze variabili bensì i metodi che ad essi si riferiscono.
La domanda che arriva spontanea a questo punto è: bene ma io posso cambiare i valori delle istanze? La risposta è ovviamente si, ma non ancora, non con quello che abbiamo visto finora. E' necessario definire un setter, ovvero un metodo che permette di settare i valori per le istanze. La cosa è semplice e non molto dissimile dalla definizione dei getter:

  Esempio 10.4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Persona
def initialize(nome, cognome)
@nome, @cognome = nome, cognome
end

def nome
@nome
end

def cognome
@cognome
end

def nome=(value)
@nome=value
end

def cognome=(value)
@cognome=value
end

end

p = Persona.new("Mario", "Rossi")
puts p.nome
puts p.cognome
p.nome = "Luigi"
puts(p.nome)

Se volete ovviamente, si può usare una scrittura a mio avviso più compatta:

  Esempio 10.5
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Persona
def initialize(nome, cognome)
@nome, @cognome = nome, cognome
end

def nome; @nome; end
def cognome; @cognome; end
def nome=(value); @nome=value; end
def cognome=(value); @cognome=value; end

end

p = Persona.new("Mario", "Rossi")
puts p.nome
puts p.cognome
p.nome = "Luigi"
puts(p.nome)

Se la scrittura compatta appaga un po' di più l'occhio non è però comunque del tutto soddisfacente in termini di chiamiamola verbosità. Per questo motivo Ruby è in grado di fornire un modo per rendere più snella la cosa; in realtà infatti, la classe class è un discendente della classe module la quale contiene tre metodi attr_reader, attr_writer e attr_accessor che risolvono il problema permettendo, rispettivamente, la lettura (getter), la scrittura (setter) o entrambe le possibilità. Ecco quindi come modificare il 10.3 e il 10.4

  Esempio 10.6
1
2
3
4
5
6
7
8
9
10
11
12
13
class Persona
def initialize(nome, cognome)
@nome, @cognome = nome, cognome
end
attr_reader :nome, :cognome
attr_writer :nome, :cognome
end

p = Persona.new("Mario", "Rossi")
puts p.nome
puts p.cognome
p.nome = "Luigi"
puts(p.nome)

oppure si potevano sintetizzare le righe 5 e 6 in un'unica istruzione:

attr_accessor :nome, :cognome

avrete senz'altro notato l'uso di simboli (ricordate? : seguito da un identificativo) ma è possibile usare anche le stringhe, quindi ad esempio:

attr_accessor "nome", "cognome"

Ovviamente attraverso attr_reader e attr_writer si può scegliere più liberamente come gestirci le istanze variabili ovvero quali presentare verso l'esterno e quali rendere modificabili all'esterno della classe. Quanto esposto conferma la tendenza di Ruby a proporre più strade per fare determinate operazioni.

Prima di proseguire dobbiamo dare uno sguardo ad un importante concetto espresso da una semplice keyword: self. Detto brevemente, self si riferisce all'oggetto stesso, un po' come una persona può riferirsi a se come "me stesso". E' un concetto apparentemente banale ma in realtà molto utile. Per vedere questo riferimento a "se stessi" vediamo questo semplice esempio:

  Esempio 10.7
1
2
3
4
5
6
7
8
9
10
11
class Persona
def initialize(nome, cognome)
@nome, @cognome = nome, cognome
end
def ciao
puts "hello from #{self}"
end
end

p = Persona.new("Mario", "Rossi")
p.ciao

Che dà il seguente output (in parte sarà diverso sul vostro pc)

hello from #<Persona:0x000000028d60b0>

la prima parte dell'output è ovviamente la classe stessa, la seconda è una locazione in memoria. Nel frattempo, di passaggio, abbiamo visto come richiamare un metodo definito in una classe: l'istruzione è alla riga 11, il punto serve per accedere, appunto, al metodo. Non c'è alcuna differenza rispetto al richiamo di un altro elemento della classe. Tornando al nostro self, esso può risultare utile ad esempio quando si debbano usare elaborazioni interne alla classe stessa:

  Esempio 10.8
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Calc
  def initialize(x1, x2)
    @x1, @x2 = x1, x2
  end
  attr_accessor :x1, :x2
  def somma
    puts x1 + x2
    puts("ora cambio i valori")
    self.x1 = 10
    self.x2 = 12
    puts(x1 + x2)
  end
end

p = Calc.new(5, 6)
p.somma

Le righe critiche sono la 9 e la 10 che attribuiscono dei nuovi valori a x1 e x2. Il programma darebbe lo stesso output anche eliminando self ma questo si verifica solo perchè in pratica creereste due nuove variabili x1 ed x2, non lavorereste su quelle esistenti. L'uso di self è peraltro abbastanza sottile e più complesso di quanto visto finora nei nostri semplici esempi, pertanto ne parleremo meglio in dettaglio in sede di approfondimento sulle classi.

Una caratteristica interessante della classi in Ruby è che essere possono essere riaperte per aggiungere o modificare delle funzionalità. La cosa può essere molto utile in alcuni casi, per ora mostro solo sommariamente il meccanismo:

  Esempio 10.9
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Uno
def one
puts "one"
end
end

u = Uno.new
u.one

class Uno
def ein
puts("ein")
end
end

e = Uno.new
e.one
e.ein

Dalla riga 10 alla 14 abbiamo riaperto la classe Uno ed abbiamo aggiunto un metodo che poi abbiamo potuto utilizzare.