Ruby language

Le classi - 3                

Quando si parla di classi sentirete sempre parlare di concetti come ereditarietà, polimorfismo ecc... Il mondo della programmazione a oggetti è molto ricco e conserva al suo interno una eccezionale potenza espressiva ed elaborativa. Detto questo, vediamo come Ruby sviluppa questi ed altri interessanti concetti.

Il primo concetto che affrontiamo è quello di ereditarietà. Molti di voi sapranno già di che cosa sto per parlare; in breve, si tratta di un concetto che lega tra di loro due classi, una di esse sarà identificata come superclasse, in soldoni si potrebbe dire quella creata per prima e l'altra è definita come sottoclasse in quanto deriva tutte o parte le sue caratteristiche dalla super classe (se volete potete chiamarle madre e figlia, o padre e figlio, o anche classe base e classe derivata, il concetto dovrtebbe essere concettualmente chiaro). Dal punto di vista della nomenclatura, ma anche pratico, si dice anche una sottoclasse estende la superclasse o le superclassi da cui deriva. Vedremo come questo abbia effettivamente un senso. L'ereditarietà può essere singola o multipla; nel primo caso una sottoclasse può discendere da una e una sola superclasse, nel secondo caso può avere più progenitori. Si tratta di una differenza sostanziale ai fini pratici. Ruby, come la maggior parte dei linguaggi, adotta il primo approccio. Tralascio le discussione relative al fatto che sia meglio l'uno o l'altro metodo; Ruby lavora adottando l'ereditarietà singola e questo è quanto. Tramite l'ereditarietà è possibile creare complesse gerarchie con molti livelli di classi. In particolare, se è vero che una sottoclasse può avere una sola superclasse è però altrettanto vero che ogni classe può avere quante sottoclassi si vogliono da essa derivanti. Quindi le struttere gerarchizzate sarnno per lo più simile ad alberi a testa in giù. Ovviamente all'inizio le vostre applicazioni non saranno particolarmente complesse da questo punto di vista.
Dal punto di vista concettuale è importante notare che la possibilità di creare gerarchie di oggetti, di classi ha evidentemente un una forte potenzialità espressiva che però non viene da sè ma deve essere creata dal programmatore; per esempio potrà anche essere possibile derivare da una superclasse "chiodo" una sottoclasse "gatto", l'interprete Ruby non ve lo impedisce di sicuro, ma ha chiaramente poco senso. Creare un'architettura di classi coerente è un compito importantissimo specialmente in applicazioni di grandi dimensioni e sulla quale debbono mettere le mani più persone. A completare le cose poi arriverà l'argomento moduli che amplierà lo spettro applicativo in maniera netta aprendo nuovi orizzonti espressivi. Insomma c'è tantissima carne al fuoco.

Ma cosa vuol dire in pratica "ereditare"? In breve, se una certa classe ha determinate proprietà una sottoclasse da essa derivante le possiede anch'essa senza bisogno di definirle. Questo non impedisce che la sottoclasse possa avere proprietà sue che non sono presenti nella superclasse (diversamente l'utilità non sarebbe gran che) ed anche, nella grande maggioranza dei linguaggi, che non possa a ridefinire ciò che eredita dalla superclasse. A qesto punto è arrivato il momento di dare uno sguardo a un po' di codice.

  Esempio 12.1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class UnSaluto
  def hi
    puts "Hi"
  end
end

class DueSaluti < UnSaluto
  def ciao
    puts "ciao"
  end
end

s1 = UnSaluto.new
s2 = DueSaluti.new
s1.hi
s2.hi
s2.ciao

Dalla riga 1 alla 5 definiamo una classe, normalmente. Dalla 7 alla 11 ne definiamo un'altra ma avrete notato quel < che altro non è che l'operatore adottato in Ruby per innescare l'ereditarietà. Questa risulta in azione alla riga 16 dove viene richiamato per l'istanza s2 della classe DueSaluti il metodo h1 che e definito nella classe UnSaluto ma non più in DueSalute, nell'ambito della quale è disponibile per eredità, appunto. Se volete potete continuare questo semplice esempio aggiungendo la classe Tresaluti definita:

class TreSaluti < Duesaluti
  def hola
    puts "hola"
  end
end

e vedrete come una sua istanza sia in grado di riprodurre tutti e 3 i saluti.
Nel caso non vi fosse noto quale sia la superclasse di una sottoclasse, Ruby mette a disposizione il metodo superclass che vi permette di risalire di un gradino nella gerarchia delle classi.

  Esempio 12.2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Super
  def hi
    puts "Hi"
  end
end

class Sub < Super
end

puts(Sub.superclass)
puts(Super.superclass)
puts(Super.superclass.superclass)
puts(Super.superclass.superclass.superclass)
puts(Super.superclass.superclass.superclass.inspect)

L'output è il seguente:

Super
Object
BasicObject

nil

La riga 10 dà come output la superclasse di Sub che è Super. La riga 11 ci fa risalire nella gerarchia nascota, Super è discendente da una generica classe Object. A sua volta Object è sottoclasse di BasicObject, la root del sistema a classi di Ruby. Non esiste progenitore di BasicObject, la riga 13 non stampa nulla perchè, come ci  mostra la 14,  sopra questa radice c'è il nulla (nil). Se poi volete sbizzarrirvi ancora di più esiste anche ancestors... fate qualche prova. Naturalmente la possibilità di ereditare è attribuibile anche alle proprietà non solo ai metodi. Il semplice esempio che segue illustra questa possibilità:

  Esempio 12.3
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
class Super
def initialize(x, y)
@x, @y = x, y
end

def x
@x
end

def y
@y
end

def x=(value)
@x = value
end

def y=(value)
@y = value
end
end

class Sub < Super
end

sb = Sub.new(1,2)
puts(sb.x)

La classe Sub ha ereditato x e y da Super ed è possible utilizzarli molto semplicemente.

Questione diversa è quella dell'overriding. In altre parola, possiamo ereditare un metodo o una proprietà da una superclasse ma potremmo anche volerne modificare in qualche modo il funzionamento. Il procedimento è molto semplice e consiste nella semplice riscrittura del metodo con le modifiche che vogliamo applicare. L'esempio che segue modifica il 12.1:

  Esempio 12.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
class UnSaluto
def hi
puts "Hi"
end
end

class DueSaluti < UnSaluto
def ciao
puts "ciao"
end
end

class TreSaluti < DueSaluti
def ciao
puts "ciao - 2"
end
end

s1 = UnSaluto.new
s2 = DueSaluti.new
s3 = TreSaluti.new
s1.hi
s2.hi
s2.ciao
s3.hi
s3.ciao

La parte evidenziata in rosso, righe dalla 14 alla 16, ridifinisce il metodo ciao, definito nella superclasse DueSaluti e cambia la stringa di output, come è facile constatare eseguendo il programma. Nulla invece accade per quanto riguarda il metodo Hi, ereditato indirettamente daalla clase UnSDaluto. Ovviamente se quest'ultimo metodo fosse stato modifica all'interno di DueSaluti la classe treSaluti avrebbe utilizzato quest'ultimo.
Ruby però dimostra in questi casi la sua elasticità. Supponiamo ad esempio di dover effettuare l'override di un metodo di una superclasse ma vorremmo anche conservare il lavoro fatto in quest'ultimo per poterlo utilizzare senza doverlo riscrivere... si può fare? Certo, attraverso la keyword alias, che usa la sintassi:

alias nomenuovo :nomemetodosuper

con i due punti che devono essere attaccati, senza spazi in mezzo quindi, al nome del metodo della superclasse che viene ridefinito nella classe corrente, come nell'esempio seguente:

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

class UnoDue < Uno
def two
puts "2"
end
end

class UnoDueTre < UnoDue
alias dos :two
def two
puts "3"
end
end

s1 = UnoDueTre.new
s1.dos
s1.two

In questo modo potete continuare ad utilizzare quanto fatto nel metodo two della classe UnoDue. In questo caso non è un problema ma in situazioni complesse è un escamotage da tenere presente. Tra l'altro quanto avete definito tramite alias è un metodo (o una proprietà) a tutti gli effetti e quindi pienamente utilizzabili da parte di una classe che discenda da UnoDueTre.