|
|||
Precedente < | Indice ^ |
Prossimo > |
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ì...
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.
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.
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.
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 |
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.
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.
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.
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 |
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.
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.
initialize
che è sempre privato ).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 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
.
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
String
con 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.