Programming Ruby

The Pragmatic Programmer's Guide

Precedente < Indice ^
Prossimo >

Classi, Oggetti, e Variabili


Dagli esempi mostrati finora, potreste essere rimasti meravigliati dalla nostra precedente affermazione, che Ruby è un linguaggio object-oriented. In questo capitolo dimostreremo la nostra affermazione. Stiamo per andare ad osservare come potrete creare in Ruby classi ed oggetti, e in quali situazioni Ruby è più performante della maggioranza dei linguaggi object-oriented. Lungo il percorso, implementeremo parte del nostro prodotto multimiliardario, il Jukebox Internet Enabled Jazz and Blue Grass.

Dopo mesi di lavoro, i nostri gruppi Ricerca e Sviluppo, riccamente pagati, hanno stabilito che il nostro jukebox ha bisogno di canzoni. Così sembrerebbe una buona idea iniziare con lo stabilire una classe Ruby che determini le caratteristiche delle canzoni. Sappiamo che una canzone ha un nome, un artista, una durata e vorremo essere certi che gli oggetti suono lo facciano anche nel nostro programma.

Partiremo con il creare una classe base Song, [Come menzionato a pagina 9, il nome delle classi incomincia con una lettera minuscola.] che contiene solamente un solo metodo, initialize.

class Song
  def initialize(name, artist, duration)
    @name     = name
    @artist   = artist
    @duration = duration
  end
end

initialize. è un metodo speciale nei programmi di Ruby. Quando chiamerete Song#new per creare un nuovo oggetto Song, Ruby creerà un oggetto inizializzato e di seguito denominerà quell'oggetto initializzato metodo, passandogli tutti i parametri che avremo passato a new. Questo percorso vi darà una possibilità per scrivere codice che inizializzano il vostro stato dell' oggetto.

Per la classe Song, il metodo initialize prende tre parametri. Questi parametri agiscono esattamente come le variabili locali all'interno del metodo, così le seguono nella convenzione dei nomi iniziando con una lettera minuscola.

Ogni oggetto rappresenta il suo proprio suono, così si rende necessario ognuno dei nostri oggetti Song per rappresentare il nome, l' artista ed la durata della canzone. Questo significa che si rende necessario immagazzinare questi valori come una variabile instanza all'interno dell' oggetto. In Ruby una variabile instanza è semplicemente un nome preceduto da un segno "at" ("@"). Nell'esempio il parametro name è assegnato alla variabile instanza @name,artist è assegnato a @artist e duration(la lunghezza della canzone in secondi) a @duration.

Testiamo l' elegante nuova classe.

aSong = Song.new("Bicylops", "Fleck", 260)
aSong.inspect » "#<Song:0x4018bfc4  @duration=260, @artist=\"Fleck\", @name=\"Bicylops\">"

Bè, sembrerebbe lavorare! Di norma, il messaggio inspect, che può essere inviato a qualsiasi oggetto, scarica l'id e la variabile instance dell'oggetto. Sembrerebbe che l' abbiamo settate correttamente....

La nostra esperienza ci insegna che durante lo sviluppo dovremo visualizzare molte volte il contenuto di un oggetto Song e che il modo di formattare di inspect lasci alquanto a desiderare. Fortunatamente, Ruby, dispone di uno standard message, to_s, che invia a qualsiasi oggetto come vuole che venga visualizzata una stringa. Proveremo con la nostra canzone.

aSong = Song.new("Bicylops", "Fleck", 260)
aSong.to_s » "#<Song:0x4018c1b8>"

Così non è molto utile --- riporta solamente l' ID dell' oggetto. Di conseguenza, riscriveremo to_s nella nostra classe. Cogliamo l' occasione per annunciarvi come descriveremo la definizione delle classi in questo libro.

In Ruby, le classi non sono mai chiuse: potrete sempre aggiungere metodi ad una classe esistente. Questo applica alla classe che avrete scritto alla pari di una standard, una classe built-in. Tutto ciò che si dovrà fare sarà di aprire una definizione di una classe esistente ed le nuove caratteristiche che vorrete specificare saranno aggiunti laggiù.

Questa caratteristica torna a fagiolo per i nostri scopi. A lungo andare attraverso questo capitolo, andremo ad aggiungere caratteristiche alle nostre classi e mostreremo unicamente le nuove definizioni dei nuovi metodi; le vecchie saranno sempre là. Ci salva dovendo ripetere cose rindondanti per ogni esempio. Naturalmente, credo, se state scrivendo questo codice da un raffazzonamento, probabilmente riunireste tutti i metodi in una definizione di classe.

Ora che ci siamo dilungati sui dettagli, ritorniamo ad aggiungere il metodo to_s alla nostra classe Song.

class Song
  def to_s
    "Song: #{@name}--#{@artist} (#{@duration})"
  end
end
aSong = Song.new("Bicylops", "Fleck", 260)
aSong.to_s » "Song: Bicylops--Fleck (260)"

Eccellente! Stiamo facendo progressi. Nonostante tutto, siamo scivolati su qualcosa di misterioso. Avevamo detto che Ruby supporta to_s per tutti gli oggetti, ma non abbiamo detto come. La risposta è dovuta all'ereditarietà, alle sottoclassi e come Ruby determini quale metodo avviare quando spedirete un messaggio ad un oggetto. Questo è un motivo per una nuova sezione, così...

Ereditarietà e Messaggi

L' ereditarietà Vi permette di creare una classe che è il raffinamento o la specializzazione di un' altra classe. Per esempio, in nostro jukebox ha il concetto di canzone, che abbiamo incapsulato nella classeSong. Poi il Marketing è venuto avanti e ci ha detto che avevamo necessità di fornire il supporto per il kareoke. Una canzone per il Kareoke è esattamente come ogni altra (non esiste l'audio, ma al momento non ce ne importa). Comunque, è associata ad insieme di testi, unitamente alle informazione del tempo. Quando il nostro jukebox suona una canzone del kareoke, le parole dovranno scorrere attraverso lo schermo di fronte al jukebox contemporaneamente alla musica.

Un approccio a questo problema è nel definire una nuova classe, KaraokeSong, che è uguale a Song, ma con una traccia di testo.

class KaraokeSong < Song
  def initialize(name, artist, duration, lyrics)
    super(name, artist, duration)
    @lyrics = lyrics
  end
end

Il ``< Song'' sulla linea della definizione di classe informa Ruby che KaraokeSong è una sottoclasse di Song. (Non meravigliatevi, questo significa che Song è una superclasse di KaraokeSong. Ci sono persone che parlano di relazione genitori-figli, così il genitore di KaraokeSong è Song). Per ora non preoccupatevi eccessivamente del metodoinitialize; parleremo in seguito della chiamata super.

Ora creiamo una KaraokeSong e controlliamo che funzioni. Nel sistema finale, il testo sarà tenuto in un oggetto che include il testo e le informazioni sul tempo. Per testare il la nostra classe, useremo una stringa. Questo è un altro benefit nell'uso dei linguaggi senza definizioni --- non abbiamo la necessità di definire ognicosa prima di iniziare il codice.

aSong = KaraokeSong.new("My Way", "Sinatra", 225, "And now, the...")
aSong.to_s » "Song: My Way--Sinatra (225)"

Bè, funziona, ma perchè il metodo to_s non mostra il testo?

La risposta, risiede nel modo con cui Ruby determina con quale metodo dovrebbero essere le chiamate quando inviate un messaggio ad un oggetto.. Quando Ruby compila l'invocazione al metodo aSong.to_s, non sà dove trovare il metodo to_s. Invece rimanda la decisione fino a che il programma non è avviato. In quel momento osserverà la classe aSong. Se la classe implementa un metodo con lo stesso nome, come il messaggio, questo metodo si avvierà. Altrimenti, Ruby cercherà un metodo nella classe genitore, e poi in quella superiore e così avanti. Se il processo non trova il metodo appropriato, verrà eseguita una speciale azione che normalmente risulta essere un errore. [In effetti, potrete intercettare questo errore; che vi permette di estrarre i metodi al momento. Questo è descritto sotto Object#method_missing a pagina 360.]

Ora torniamo la nostro esempio. Abbiamo spedito il messaggio to_s a aSong, un oggetto della classe KaraokeSong. Ruby cerca in KaraokeSong un metodo denominato to_s, ma non lo trova. L' interprete poi cerca nel genitore di KaraokeSong, la classe Song, e là trova il metodo to_s che abbiamo definito a pagina 20. Questo è il motivo per cui mostra i dettagli della canzone ad eccezione del testo --- la classe Song non ne sà nulla di testi! ---

Ora fissiamo la cosa implementano KaraokeSong#to_s. Ci sono molti modi per fare questo. Inizieremo nel modo sbagliato. Copieremo il metodo to_s da Song e lo aggiungeremo al testo.

class KaraokeSong
  # ...
  def to_s
    "KS: #{@name}--#{@artist} (#{@duration}) [#{@lyrics}]"
  end
end
aSong = KaraokeSong.new("My Way", "Sinatra", 225, "And now, the...")
aSong.to_s » "KS: My Way--Sinatra (225) [And now, the...]"

Abbiamo correttamente mostrato il valore della variabile istanza @lyrics. Per fare questo, la sottoclasse accede direttamente alla variabile istanza del suo genitore. Allora, perche questo è un modo sbagliato per implementare to_s?

La risposta stà nella buon stile di programmazione ( e qualcosa denominato decoupling). Gingillandoci nella stato interno del nostro genitore, ci siamo rigidamente legati alle sue implementazioni. Diciamo di voler modificare Song per conservare la durata in millisecondi. Improvvisamente, KaraokeSong inizierebbe a riportare valori ridicoli. L' idea che la versione kareoke di ``My Way'' che duri per 3750 minuti è un' idea troppo spaventosa da considerare.

Potremo aggirare questo problema fornendo ad ognuna delle classi il suo proprio stato interno. Quando KaraokeSong#to_s è chiamata, dovremo chiamare il suo metodo genitore to_s per ottenere alcuni dettagli. Poi aggiungerà a questo le informazioni del testo e ritornerà il risultato. Quì il trucco è nella parola Ruby ``super''. Quando invocate super senza argomenti, Ruby invia un messaggio ai genitori dell' oggetto, chiedendogli di invocare un metodo con lo stesso nome del metodo invocato, e passandogli i parametri che sono stati passati al metodo corrente. Ora possiamo implementare il nostro nuovo e migliore to_s.

class KaraokeSong < Song
  # Formatta ourselves come una stringa aggiungendo
  # il nostro lyrics al valore #to_s del nostro progenitore.
  def to_s
    super + " [#{@lyrics}]"
  end
end
aSong = KaraokeSong.new("My Way", "Sinatra", 225, "And now, the...")
aSong.to_s » "Song: My Way--Sinatra (225) [And now, the...]"

Abbiamo detto esplicitamente a Ruby che KaraokeSong era una sottoclasse di Song, ma non abbiamo specificato una classe genitore per la stessa Song. Se non lo specificate, Ruby sopperirà con la classe Object. Questo significa che tutti gli oggetti avranno Object come progenitore, e che il metodo istanza di Object sono disponibili a tutti gli oggetti in Ruby. Ritornando a pagina 20, abbiamo detto che to_s è disponibile per tutti gli oggetti. Ora abbiamo la motivazione; to_s è uno dei 35 metodi istanza della classe Object. La lista completa incomincia a pagina 356.

Ereditarietà e Mixins

Alcuni linguaggi object oriented ( grossomodo il C++) supportano l'ereditarietà multipla dove una classe può avere più di un diretto genitore ereditando la funzionalita di ognuno. Sebbene potente questa tecnica può essere pericolosa, poichè la gerarchia dell'ereditarietà può diventare ambigua.

Altri linguaggi come per esempio il java supportano l'ereditarietà' singola. Qui, una classe puo' avere solamente un genitore diretto. Sebbene più chiaro (e più facile da implementare), la singola ereditarietà ha anche inconvenienti nel mondo reale le cose spesso ereditano gli attributi di molteplici sorgenti (per esempio una palla è contemporaneamente un oggetto rimbalzante e una sfera).

Ruby offre un compromesso interessante e potente, fornendovi la semplicità di una ereditarietà singola e la potenza delle multiple. Una classe Ruby può avere solamente un genitore diretto, e in questo modo Ruby e' un linguaggio ad ereditarietà singola. Comunque le classi Ruby possono includere la funzionalità di un numero illimitato di Mixin ( un mixin e' paragonabile ad una definizione di classe parziale). Questo fornisce una capacità di controllo assimilabile ad una ereditarietà multipla con nessun inconveniente. Esploreremo i Mixin allinizio della pagina 100.

Fino a questo punto del capitolo abbiamo dato un occhiata alle classi ed ai loro metodi. Ora e' tempo di perlustrare gli oggetti come le istanze della classe Song.

Oggetti e attributi

Gli oggetti Song che abbiamo creato finora hanno una condizione interna ( come il titolo e l'artista della canzone ). Questa condizione e' limitata a quegli oggetti ( nessun'altro oggetto puo'accedere alle variabili istanza dell'oggetto). In generale questa è una Buona Cosa. Cio' significa che l'oggetto e' esclusivamente responsabile della manutenzione e della sua solidità.

Comunque, un oggetto che è completamente nascosto e' difficilmente utilizzabile ( si può creare, ma poi non ci si puo fare nulla). Voi definirete normalmente i metodi che vi permetteranno di accedere e manipolare lo stato d'essere di un oggetto permettendo al mondo esterno di interagire con l'oggetto). Queste sfaccettature dell'oggetto visibile esternamente sono chiamate attributi.

Per i nostri oggetti Song, la prima cosa di cui abbiamo bisogno è l'abilità di scoprire il titolo, l'artista ( così possiamo mostrarli mentre la canzone sta suonando) e la durata ( così da poter mostrare una specie di barra di progressione).

class Song
  def name
    @name
  end
  def artist
    @artist
  end
  def duration
    @duration
  end
end
aSong = Song.new("Bicylops", "Fleck", 260)
aSong.artist » "Fleck"
aSong.name » "Bicylops"
aSong.duration » 260

Qui abbiamo definito tre metodi accessori per ritornare i valori delle 3 istanze di attributi. Poichè questo è una specie di lingua comune Ruby provvede un'abbreviazione conveniente: attr_reader crea questo metodo accessorio per te.

class Song
  attr_reader :name, :artist, :duration
end
aSong = Song.new("Bicylops", "Fleck", 260)
aSong.artist » "Fleck"
aSong.name » "Bicylops"
aSong.duration » 260

Questo esempio ha introdotto qualcosa di nuovo. Il costrutto :artist è un'espressione che ritorna un oggetto Symbol in corrispondenza ad artist. Voi potrete pensare ad :artist come al significato del nome della variabile artist, mentre il semplice artist è il valore della variabile. In questo esempio abbiamo nominato i metodi accessori name, artist e duration. Le corrispondenti variabili istanze, @name, @artist, e @duration, saranno create automaticamente. Questi metodi accessori sono identici a quelli scritti a mano precedentemente.

Attributi Scrivibili

Talvolta necessiterete di essere in grado di impostore un attributo dall'esterno dell'oggetto. Per esempio, assumiamo che la durata inizialmente associata a una canzone sia una stima ( forse assunta dalle informazioni sul cd o sui dati del MP3). La prima volta che suoniamo una canzone scopriremo quanto e' veramente lunga e immagazineremo il nuovo valore nell'oggetto Song.

Nei linguaggi come il C++ e il java, lo dovreste fare con le funzioni di impostazione.

class JavaSong {                     // Java code
  private Duration myDuration;
  public void setDuration(Duration newDuration) {
    myDuration = newDuration;
  }
}
s = new Song(....)
s.setDuration(length)

In Ruby gli attributi di un'oggetto possono essere accessibili come se fossero una qualsiasi altra variabile. Abbiamo visto ciò a proposito delle espressioni come aSong.name. Così, potrà sembrare naturale assegnare queste variabili quando vorrete settare il valore di un attributo. In armonia col principio dell'ultima sorpresa questo è quello che fareste in Ruby.

class Song
  def duration=(newDuration)
    @duration = newDuration
  end
end
aSong = Song.new("Bicylops", "Fleck", 260)
aSong.duration » 260
aSong.duration = 257   # setta l'attributo con il nuovo valore
aSong.duration » 257

L'assegnamento ``aSong.duration = 257'' invoca il metodo duration= nell'oggetto aSong, passandogli 257 come argomento. In fatti, definendo il nome di un metodo che termini con un segno di uguale fornisce a quel nome i diritti di apparire alla sinistra di un assegnamento.

Di nuovo, Ruby fornisce una scorciatoia per creare questi semplici metodi di assegnazione.

class Song
  attr_writer :duration
end
aSong = Song.new("Bicylops", "Fleck", 260)
aSong.duration = 257

Attributi Virtuali

Questi metodi di accesso agli attributi non devono essere come delle semplici coperte attorno alle variabili istanza dell' oggetto. Per esempio, potreste voler accedere alla durata in minuti e in frazioni di minuto piuttosto che in secondi come abbiamo finora fatto.

class Song
  def durationInMinutes
    @duration/60.0   # forza il decimale
  end
  def durationInMinutes=(value)
    @duration = (value*60).to_i
  end
end
aSong = Song.new("Bicylops", "Fleck", 260)
aSong.durationInMinutes » 4.333333333
aSong.durationInMinutes = 4.2
aSong.duration » 252

Quì abbiamo usato il metodo attribuito per creare una variabile d'istanza virtuale. Per il mondo esterno, durationInMinutes sembra essere un attributo come qualsiasi altro. Internamente, però, non c'è nessuna corrispondente variabile istanza.

Questo è più di una curiosita. In questo libro Object-Oriented Software Construction , Bertrand Meyer chiama questo il principio di accesso uniforme. Eliminando le differenze tra le variabili istanza e i valori calcolati, state difendendo il resto del mondo dall' implementazione della tua classe. Tu sei libero di cambiare il funzionamento delle cose nel futuro senza comprendere le milioni di linee di codice che usate nella vostra classe. Questa è una grossa vittoria.

Variabili Classe e Metodi Classe

Finora, tutte le classi che abbiamo creato hanno contenuto istanze di variabili e istanze di metodi: le variabili sono state associate con una particolare istanza della classe e i metodi che agiscono su queste variabili. Qualche volta le classi stesse necessitano di avere le proprie condizioni. Quì è dove la variabile classe interviene.

Variabili Classe

Una variabile classe è condivisa tra tutti gli oggetti di una classe ed è anche accessibile ai metodi di classe come descriveremo più avanti. C' è solo una copia di una particolare variabile di classe per la classe data. I nomi di variabile classe iniziano con il simbolo ``at'' come ``@@count''. Diversamente alle variabili globali e istanza, le variabili di classe devono essere inizializzate prima di essere usate. Spesso questa inizializzazione è semplicemente una assegnazione nel corpo della definizione della classe.

Per esempio il nostro jukebox può voler registrare il numero di volte che quella particolare canzone è stata suonata. Questo contatore potrebbe essere una variabile istanza dell' oggetto Song. Quando una canzone è eseguita, è incrementato il valore della variabile. Ma noi diciamo che vogliamo sapere anche quante canzoni sono state eseguite in totale. Potremmo farlo cercando tutti gli oggetti Song e sommando i loro contatori, oppure correremo il rischio di una scomunica dalla Chiesa del Buon Disegno e utilizzare una variabile globale. Invece, utilizzeremo una variabile di classe.

class Song
  @@plays = 0
  def initialize(name, artist, duration)
    @name     = name
    @artist   = artist
    @duration = duration
    @plays    = 0
  end
  def play
    @plays += 1
    @@plays += 1
    "This  song: #@plays plays. Total #@@plays plays."
  end
end

Per scopi di debug, abbiamo predisposto per Song#play che ritorni una stringa contenente il numero di volte è stata suonata questa canzone, con tutto il numero di esecuzioni di tutte le canzoni. Possiamo testarlo facilmente.

s1 = Song.new("Song1", "Artist1", 234)  
s2 = Song.new("Song2", "Artist2", 345)
s1.play » "This  song: 1 plays. Total 1 plays."
s2.play » "This  song: 1 plays. Total 2 plays."
s1.play » "This  song: 2 plays. Total 3 plays."
s1.play » "This  song: 3 plays. Total 4 plays."

Le variabili Classe sono private ad una classe ed alle sue istanze. Se volessi renderle accessibili al mondo esterno, dovrai scrivere un metodo accessorio. Questo metodo potrebbe essere sia un metodo di istanza o, lasciandoci guidare fino alla prossima sezione, un metodo di classe.

I Metodi di Classe

Qualche volta una classe necessita di fornire metodi che lavorino senza essere legati ad un qualsiasi particolare oggetto.

Ne abbiamo già incontrato uno. Il metodo new crea una nuovo oggetto Song, ma il medesimo non è associato ad una particolare canzone.

aSong = Song.new(....)

Troverete i metodi di classe sparsi ovunque nelle librerie di Ruby. Per esempio, gli oggetti di una classe File simboleggiano l' apertura dei file nel filesystem sottostante. Comunque, la classe File fornisce inoltre molti metodi di classe per manipolare archivi che non sono aperti e quindi non hanno un oggetto File. Se vuoi cancellare un file chiamerai il metodo di classe File::delete, passandogli come argomento il nome.

File.delete("doomedFile")

I metodi di classe sono distinti dai metodi d' istanza tramite la loro definizione. I metodi di classe sono definiti ponendo il nome di classe e un punto davanti al nome del metodo.

class Example

  def instMeth              # metodo instanza
  end

  def Example.classMeth     # metodo di classe
  end

end

Il jukeboxes richiede denaro per ogni canzone suonata, non per il tempo impegnato. Questo rende le canzoni brevi più vantaggiose di quelle lunghe. Potremo voler ostacolare le canzoni che richiedono troppo tempo per essere disponibili nella lista delle canzoni. Potremo definire un metodo di classe in SongList che una volta richiamato verifichi se una canzone particolare ecceda il limite. Imposteremo questo limite utilizzando una costante di classe, che è semplicemente una costante (ricordi le costanti? Esse iniziano con una lettera maiuscola) inizializzata nel corpo della classe stessa.

class SongList
  MaxTime = 5*60           #  5 minuti
  def SongList.isTooLong(aSong)
    return aSong.duration > MaxTime
  end
end
song1 = Song.new("Bicylops", "Fleck", 260)
SongList.isTooLong(song1) » false
song2 = Song.new("The Calling", "Santana", 468)
SongList.isTooLong(song2) » true

Oggetti unici ed altri costruttori

Talvolta potrete voler sovrascrivere il metodo standard nel quale Ruby crea gli oggetti. Come esempio diamo un occhiata al nostro jukebox. Poichè avremo molti jukebox sparpagliati lungo il paese, vogliamo fare la manutenzione il più facilmente possibile. Parte dei requisiti è di registrare qualsiasi cosa che accade ad un jukebox: le canzoni che sono state suonate, i soldi ricevuti, gli strani liquidi versati e così di seguito. Poichè ne vogliamo preservare l' ampiezza di banda della rete per la musica, memorizzeremo tali archivi di registrazione localmente. Ciò significa che necessiteremo di una classe che controlli le operazioni di memorizzazione. Comunque, ne vogliamo solo uno per jukebox, e vogliamo che l' oggetto sia condiviso tra gli altri oggetti che lo usano.

Inserite il modello Singeton, documentato nei Modelli di Disegno . Faremo in modo così che il solo metodo per creare un oggetto di memorizzazione sia chiamare Logger#create, e così saremo sicuri che solo un oggetto di memorizzazione è stato creato.

class Logger
  private_class_method :new
  @@logger = nil
  def Logger.create
    @@logger = new unless @@logger
    @@logger
  end
end

Dichiarando privato il metodo new di Logger, faremo in modo di prevenire chiunque dal creare un oggetto di registrazione utilizzando il costruttore convenzionale. Invece, noi forniamo un metodo di classeLogger#create. Utilizziamo la variabile di classe @@logger per ottenere un riferimento ad una singola istanza del registratore, ritornando tale istanza tutte le volte che è chiamato. [L' implementazione di un oggetto unico che quì presentiamo non è thread-safe; se molti sottoprocessi fossero in esecuzione potrebbe essere possibile creare molti oggetti registratore. Comunque piuttosto che aggiungere da noi stessi un sottoprocesso di sicurezza, dovremo utilizzare il mixins Singleton fornito con Ruby, che è documentato a pagina 472.] Possiamo verificarlo osservando gli identificatori dell' oggetto che il metodo restituisce.

Logger.create.id » 537684700
Logger.create.id » 537684700

Utilizzando il metodo di classe come un pseudo costruttore possiamo anche rendere la vita più semplice per gli utilizzatori della sua classe. Come esempio da poco, osserviamo una classe Shape che rappresenta un poligono regolare. Le istanze di Shape sono create fornendo al costruttore il numero richiesto di lati e il perimetro totale.

class Shape
  def initialize(numSides, perimeter)
    # ...
  end
end

Comunque un paio di anni dopo questa classe è usata in un'applicazione differente, dove i programmatori sono soliti creare generi per nome, e specificando la lunghezza del lato non il perimetro. Semplicemente aggiungendo alcuni metodi di classe a Shape.

class Shape
  def Shape.triangle(sideLength)
    Shape.new(3, sideLength*3)
  end
  def Shape.square(sideLength)
    Shape.new(4, sideLength*4)
  end
end

Esistono numerosi interessanti e potenti usi di un metodo di classe, ma esplorandoli non otterremo di finire alcun jukebox presto, così muoviamoci.

Controllo di accesso

Quando stiamo disegnando un'interfaccia di una classe è importante considerare quanto accesso alla vostra classe vorrete esporre al mondo esterno. E' preferibile concedere molto accesso alla vostra classe, con il rischio di 'aumentare le dupicazioni nell'applicazione ( gli utenti della classe saranno tentati di confidare i dettagli dell' implementazione di classe), piuttosto che all'interfaccia logica. Le buone notizie sono che l'unico modo di cambiare lo stato dell'oggetto è possibile chiamando uno dei suoi metodi. Controllate l' accesso ai metodi e avrete controllato l'accesso all'oggetto. Una buona regola di azione è di non esporre mai i metodi che potrebbero lasciare un oggetto in uno stato non valido. Ruby fornisce 3 livelli di protezione.

La differenze tra "protetto" e privato e' abbastanza sottile, ed è più differente in Ruby rispetto ai più comuni linguaggi OO. Se un metodo è protetto deve essere chiamato da una qualsiasi istanza di una classe o sottoclasse definita. Se un metodo è privato deve essere chiamato solo senza il contesto dell'oggetto chiamato ( non è mai possibile accedere ad altri metodi privati dell'oggetto direttamente anche se l'oggetto è della stessa classe del chiamante.

Ruby differisce dagli altri linguaggi OO in un altro modo importante. Il controllo di accesso e' determinato dinamicamente, come il programma và in esecuzione, non staticamente. Otterrete una violazione di accesso quando il codice attende di eseguire un metodo ristretto.

Specifici Controlli di Accesso

Specifici i livelli di accesso ai metodi all'interno della definizione di una classe o di un modulo utilizzando una o più delle tre funzioni public, protected, e private. Ogni funzione può essere utilizzata in due differenti modi.

Se usate senza ulteriori argomenti, le tre funzioni impostano il controllo di accesso primario dei metodi definiti successivamente. Ciò è probabilmente un comportamento familiare se siete dei programmatori del C++ o Java, dove potreste utilizzare parole come public per eseguire lo stesso effetto.

class MyClass

      def method1    # default è 'public'
        #...
      end

  protected          # i metodi seguenti saranno 'protected'

      def method2    # ora è 'protected'
        #...
      end

  private            # i metodi seguenti saranno 'private'

      def method3    # ora è 'private'
        #...
      end

  public             # i metodi seguenti saranno 'public'

      def method4    # e questo è 'public'
        #...
      end
end

Alternativamente, potete definire i livelli di accesso dei metodi nominati elencandoli come argomento per accedere alle loro funzioni.

class MyClass

  def method1
  end

  # ... e così avanti

  public    :method1, :method4
  protected :method2
  private   :method3
end

Il metodo initialize di una classe è automaticamente dichiarato privato.

E' l' ora di qualche esempio. Forse stiamo realizzando un sistema di contabilità dove ogni debito ha un corrispondente credito. Poichè vogliamo assicurare che nessuno possa interrompere questa regola, creeremo il metodo che renda i debiti ed i crediti privati, e definiremo la nostra interfaccia esterna in termini di astrazione.

class Accounts

  private

    def debit(account, amount)
      account.balance -= amount
    end
    def credit(account, amount)
      account.balance += amount
    end

  public

    #...
    def transferToSavings(amount)
      debit(@checking, amount)
      credit(@savings, amount)
    end
    #...
end

L' accesso protetto è usato quando gli oggetti hanno necessità di accedere allo stato interno di altri oggetti della stessa classe. Per esempio, dobbiamo voler permettere ad ogni oggetto Account di comparare their raw balances, ma dobbiamo voler hide those balances dal resto del mondo (forse perchè le presentiamo in forma differente).

class Account
  attr_reader :balance       # metodo accesorio 'balance'

  protected :balance         # lo rende protetto

  def greaterBalanceThan(other)
    return @balance > other.balance
  end
end

Poichè l' attributo balance è protetto, è disponibile solamente all'interno degli oggetti Account.

Variabili

Ora, che siamo arrivati al problema di creare tutti questi oggetti, facciamo sì di non perderli. Le variabili sono utilizzate per tener traccia degli oggetti; ogni variabile contiene un riferimento ad un oggetto.

Figure not available...

Confermiamolo con del codice.

person = "Tim"
person.id » 537684980
person.type » String
person » "Tim"

Nella prima linea, Ruby crea un nuovo oggetto String con il valore ``Tim''. Un riferimento a questo oggetto è posizionato nella variabile locale person. Una veloce verifica mostra che la variabile ha in verità assunto la personalità di una stringa, con un oggetto id, tipo e valore.

Così, si può dire che una variabile sia un oggetto?

In Ruby, la risposta è ``no''. Una variabile è semplicemente un riferimento ad un oggetto. Gli oggetti si muovono in una grossa pozza da qualche parte e sono puntati alle variabili.

Ora facciamo un esempio un pochino più complicato.

person1 = "Tim"
person2 = person1
person1[0] = 'J'
person1 » "Jim"
person2 » "Jim"

Che cosa è successo? Abbiamo cambiato il primo carattere di person1, ma entrambi person1, e person2, sono cambiate da ``Tim'' a ``Jim''.

E' tornato indietro al momento in cui la variabile manteneva il riferimento agli oggetti, non agli oggetti loro stessi. L' assegnamento della person1 alla person2 non crea un nuovo oggetto; esso copia semplicemente il riferimento all'oggetto della person1 alla person2, così che sia person1 che person2 si riferiscono allo stesso oggetto. lo mostriamo nella figura 3.1 di pagina 33.

L' assegnazione degli oggetti aliases, fornisce le risorse di molte variabili che riferiscono allo stesso oggetto. Ma ciò potrebbe causare problemi nel codice? Lo può, ma non così spesso come pensereste ( gli oggetti in java, per esempio, lavorano nello stesso modo). Per esempio, nella figura 3.1, potreste permette di creare degli alias usando il metodo dup di String, che crea un nuovo oggetto Stringcon identici contenuti.

person1 = "Tim"
person2 = person1.dup
person1[0] = "J"
person1 » "Jim"
person2 » "Tim"

Potrete prevenire chiunque dal modificare un particolare oggetto congelandolo ( ne parleremo approfonditamente da pagina 255). Tenta di modificare un oggetto congelato, e Ruby alzerà una eccezione TypeError.

person1 = "Tim"
person2 = person1
person1.freeze       # previene la modificazione dell' oggetto
person2[0] = "J"

produce

prog.rb:4:in `=': can't modify frozen string (TypeError)
        from prog.rb:4


Precedente < Indice ^
Prossimo >

Extracted from the book "Programming Ruby - The Pragmatic Programmer's Guide"
Copyright © 2000 Addison Wesley Longman, Inc. Released under the terms of the Open Publication License V1.0.
This reference is available for download.