|
|||
Precedente < | Indice ^ |
Prossimo
> |
I moduli sono un modo di ragguppare insieme i metodi, le classi e le costanti. I moduli vi forniscono due ulteriori benefici:
Nel momento in cui inizierete a scrivere programmi sempre più grandi in Ruby, troverete normale produrre grossi blocchi di codice che potrete riutilizzare in futuro --- librerie di routines messe in correlazione che sono applicabili in molti programmi. Vorrete spezzare questo codice in file distinti in modo che i contenuti possano essere utilizzati da differenti programmi.
Spesso questo codice sarà organizzato in classi, di conseguenza probabilmente appicicherete una classe ( o un insieme di classi interlacciate ) in un file.
Comunque, vi troverete in situazioni in cui vorrete ragguppare insieme delle cose che non formano in modo naturale una classe.
Un approccio iniziale potrebbe essere quello di riunire tutte queste cose
in un file e semplicemente caricare quel file in tutti i programmi che lo
necessitano. Questa è la procedura di lavoro del linguaggio C.
Fondamentalmente esiste un problema, Dite di voler scrivere un insieme di
funzioni trigonometriche: sin
, cos
, e via dicendo.
Le salvate tutte in un file, trig.rb
, da beneficiarne in future
generazioni. Nel frattempo, Sally sta lavorando ad un simulatore del bene e
del male, e ha creato un insieme di proprie utili routine, tra cui
beGood
e sin
(peccato), e le memorizza in
action.rb
. Joe, che vuole scrivere un programma per scoprire
quanti angeli sono in grado di ballare sulla capocchia di uno spillo, ha
bisogno di caricare sia trig.rb
che action.rb
nel
suo programma. Ma entrabi definisconoun metodo che viene chiamato
sin
. Cattive notizie....
La risposta è il meccanismo del modulo. I moduli definiscono un insieme di nomi in cui tutti i nomi sono unici (namespace), una sabbiera nella quale i vostri metodi e le vostre costanti possono essere usate senza doversi preoccupare di essere soppravanzate da altri metodi e costanti. Le funzioni trigonometriche possono andare all'interno di un modulo:
module Trig PI = 3.141592654 def Trig.sin(x) # .. end def Trig.cos(x) # .. end end |
e i metodi delle azini buone e cattive possono andare in un'altro:
module Action VERY_BAD = 0 BAD = 1 def Action.sin(badness) # ... end end |
I moduli delle costanti sono nominati esattamente come la classe di costanti, con un iniziale lettera maiuscola. Anche le definizioni di metodo sembrano simili: questi metodi di modulo sono definiti esattamente come i metodi di classe.
Se un terzo programma volesse fruire di questi moduli, potrebbe
semplicemente caricare i due file (utilizzando l'asserzione di Ruby
require
, di cui ne abbiamo già parlato a pagina 105) e
assegnando i nomi qualificati.
require "trig" require "action" y = Trig.sin(Trig::PI/4) wrongdoing = Action.sin(Action::VERY_BAD) |
Come con i metodi di classe, potreste chiamare un metodo di un modulo precedendo il suo nome con il nome del modulo ed un punto, e assegnando una costante utilizzando il nome del modulo e due volte due punti .
I Moduli hanno un altro, utilizzo fantastico. In un sol colpo, quasi eliminano la necessità di ereditarietà multipla, fornendo una caratteristica chiamata mixin.
Negli esempi della sezione precedente, abbiamo definito i metodi dei moduli, metodi i cui nomi sarebbero prefissati dal nome del modulo. Se la cosa ti fa pensare ai metodi di classe, il vostro prossimo pensiero potrebbbe essere ``che cosa succede se definisco i metodi istanza all'interno di un modulo?'' Buona domanda. Un modulo non può avere le istanze, poichè un modulo non è una classe. Comunque, potresteincludere un modulo all'interno di una definizione di classe. Quando la cosa succede, tutti i metodi delle istanze dei moduli diventano immediatamente disponibili come pure i metodi della classe . Acquistano la condizione di mixed in. Infatti, i moduli mixed-in si comportano effettivamente com una superclasse.
module Debug |
||
def whoAmI? |
||
"#{self.type.name} (\##{self.id}): #{self.to_s}" |
||
end |
||
end |
||
class Phonograph |
||
include Debug |
||
# ... |
||
end |
||
class EightTrack |
||
include Debug |
||
# ... |
||
end |
||
ph = Phonograph.new("West End Blues") |
||
et = EightTrack.new("Surrealistic Pillow") |
||
|
||
ph.whoAmI? |
» | "Phonograph (#537683810): West End Blues" |
et.whoAmI? |
» | "EightTrack (#537683790): Surrealistic Pillow" |
Includendo il modulo Debug
, sia Phonograph
che
EightTrack
guadagnano l'accesso al metodo istanza
whoAmI?
Chairiamo un paio di punti sul comando include
prima di
procedere. Il primo, non ha nulla a che fare con i file. I programmatori in C
utilizzano una direttiva per il preprocessore chiamata #include
per inserire i contenuti di un file all'interno di un altro durante la
compilazione. Il comando Ruby in
clude
crea
semplicemente un riferimento ad un nome di un modulo. Se questo modulo si
trova in un file separato, dovrete usare require
per trascinare
quel file prima di usare include
. Secondo, Un
include
in Ruby non copia semplicemente i metodi d'istanza del
modulo dentro la classe. Invece, crea un riferimento dalla classe al modulo
incluso. Se più classi sono incluse in quel modulo, punteranno tutte alla
stessa cosa. Se cambiaste la definizione di un metodo all'interno di un
modulo, anche con il prgramma in esecuzione, tutte le classi che includono
quel modulo esibiranno il nuovo comportamento.[Naturalmente, stiamo
parlando solamente di metodi. Le variabili istanza, per esempio, sono sempre
per gli oggetti.]
I Mixin forniscono un meraviglioso modo controllato di aggiungere
funzionalità alla classe. Comunque, la loro vera potenza si mostra quando il
codice nel mixin inizia ad iteragire con il codice nella classe che lo
utilizza. Prendiamo ad esempio il mixin standard di Ruby
Comparable
. Il mixin Comparable
può essere usato
per aggiungere gli operatori di comparazione (<, <=, ==, >= e
>
) come il metodo between?
alla classe. Per questo
lavoro Comparable
assume che ogni classe che lo utilizza
definisce l'operatore <=>
. Così, come uno scrittore di
classe, definirete un metodo, <=>
che include
Comparable
, e ottiene sei funzioni di comparazione
gratuitamente. Proviamo con la nostra classe Song
, assumendo la
base di comparazione della canzone la propria durata. Tutto ciò che dovremo
fare sarà di includere il modulo Comparable
ed implementare
l'operatore di comparazione <=>
.
class Song include Comparable def <=>(other) self.duration <=> other.duration end end |
Possiamo verificare che i risultati sono sensibili con un test di poche canzoni.
song1 = Song.new("My Way", "Sinatra", 225) |
||
song2 = Song.new("Bicylops", "Fleck", 260) |
||
|
||
song1 <=> song2 |
» | -1 |
song1 < song2 |
» | true |
song1 == song1 |
» | true |
song1 > song2 |
» | false |
Finalmente, indietro a pagina 45 abbiamo mostrato un'implementazione della
funzione dello Smaltalk inject
, implementandola all'interno di
una classe Array
. Avevamo poi promesso che l'avremo resa
applicabile più in generale. Quale modo migliore che farlo con un modulo
mixin?
module Inject def inject(n) each do |value| n = yield(n, value) end n end def sum(initial = 0) inject(initial) { |n, value| n + value } end def product(initial = 1) inject(initial) { |n, value| n * value } end end |
Possiamo testarlo mischiandolo all'interno di alcune classi già esistenti.
class Array |
||
include Inject |
||
end |
||
[ 1, 2, 3, 4, 5 ].sum |
» | 15 |
[ 1, 2, 3, 4, 5 ].product |
» | 120 |
class Range |
||
include Inject |
||
end |
||
(1..5).sum |
» | 15 |
(1..5).product |
» | 120 |
('a'..'m').sum("Letters: ") |
» | "Letters: abcdefghijklm" |
Per esempi più estesi, date un'occhiata alla documentazione fornita per il
modulo Enumerable
, che inizia a pagina 407.
Chi arriva a Ruby dal C++ spesso si chiede: ``Cosa accade alle variabili di istanza in un mixin? Nel C++ si deve passare attraverso alcuni hoops per controllare in che modo le variabili siano condivise entro una gerarchia di ereditarietà multiple. Come gestisce ciò Ruby?''
Bene, per chi inizia non é davvero una domanda corretta, lo si é detto.
Rammentate come le variabili di istanza operino in Ruby: il primo accenno di
una variabile prefissata da ``@ crea una variabile di istanza entro l'oggetto
corrente, self
.
Per un mixin ciò significa che il modulo che viene unito all'interno della
vostra classe cliente (la mixee?) può creare variabili di istanza
nell'oggetto cliente e può usare attr
ed amici per definire i
modi di accedere per quelle variabili di istanza. Ad esempio:
module Notes attr :concertA def tuning(amt) @concertA = 440.0 + amt end end class Trumpet include Notes def initialize(tune) tuning(tune) puts "Instance method returns #{concertA}" puts "Instance variable is #{@concertA}" end end # The piano is a little flat, so we'll match it Trumpet.new(-5.3) |
produce:
Instance method returns 434.7 Instance variable is 434.7 |
Non soltanto il fatto di avere accesso ai metodi definiti nel mixin, ma anche di averlo alle necessarie variabili di istanza. C'é un rischio, ovviamente, che mixins diversi possano utilizzare una variabile di istanza con il medesimo nome, creando una collisione:
module MajorScales def majorNum @numNotes = 7 if @numNotes.nil? @numNotes # Ritorna 7 end end module PentatonicScales def pentaNum @numNotes = 5 if @numNotes.nil? @numNotes # Ritorna 5? end end class ScaleDemo include MajorScales include PentatonicScales def initialize puts majorNum # Dovrebbe essere 7 puts pentaNum # Dovrebbe essere 5 end end ScaleDemo.new |
produce:
7 7 |
I due pezzi di codice che abbiamo messo insieme usano entrambi una
variabile di istanza chiamata @numNote
s. Malauguratamente il
risultato non é quello che l'autore probabilmente aveva inteso ottenere.
Prevalentemente, i moduli mixin non tentano di portare in giro i loro propri
dati di istanza---essi usano accessors per rientrare in possesso dei dati
dall'oggetto cliente. Ma se doveste creare un mixin che dovesse possedere un
proprio stato, assicuratevi che le variabili di istanza abbiano nomi non
repkicati per distinguerli da qualsiasi altro mixin entro il sistema (forse
usando il nome del modulo come parte del nome della variabile).
Avrete probabilmente osservato che la raccolta di classi di Ruby supporti un ampio numero di operazioni che fanno molte cose con la raccolta: esaminarla, ordinarla e così via. Potreste pensare, ``Sarebbe certamente bello se la mia classe supportasse anche tutte queste belle caratteristiche! "' (Se voi lo pensaste ancora é forse il caso che smettiate di guardare le repliche televisive degli anni '60.)
Bene, le vostre classi possono supportare tutte queste belle
caratteristiche, grazie alla magia dei mixins e del modulo
Enumerable
. Tutto ciò che va fatto é scrivere un iteratore
each
, che restituisca subito dopo gli elementi della vostra
raccolta. . Il mixin Enumerable
, e improvvisamente la vostra
classe supporta cose come descrivono, include?
, e
find_all?
. Se gli oggetti della vostra raccolta implementano
significativi ordinamenti semantici mediante il metodo
<=
>, potrete anche ottenere min
, max
,
and sort
.
Poiché Ruby riesce facilmente a farvi scrivere del buon codice modulare, voi dovrete ritrovare sovente voi stessi creando piccoli file che contengano un bel po' delle funzionalità indipendenti --- un'interfaccia ad x, un algoritmo per ottenere y, e così via. Tipicamente organizzerete questi file come classi o librerie di moduli.
Dopo aver scritto questi files, potreste volerli incorporare entro i vostri nuovi programmi. Ruby prevede due comandi per fare ciò.
load "filename.rb" require "filename" |
Il metodo load
contiene il file sorgente di Ruby chiamato
ogni qual volta che il metodo viene eseguito, mentre require
carica ogni file dato solo una volta. require
possiede una
ulteriore funzionalità: può caricare le librerie binarie condivise. Entrambe
le routines accettano paths assoluti o relativi. Se si da un path relativo (o
solo un semplice nome), verrà effettuata la ricerca in ogni directory
compresa nel path corrente per il file ($:
, discusso a pagina
140)
I files caricati usando load
e require
possono,
evidentemente, includere altri file, i quali ne includeranno ancora altri, e
così via. Ciò che potrebbe non essere chiaro é che
require
sia un comando eseguibile --- può essere interno ad
un'istruzione if
, o può contenere una stringa appena scritta.
Il percorso di ricerca può essere facilmente cambiato durante l'esecuzione.
Basta aggiungere la directory desiderata alla stringa $:
.
Poiché load
includerà il sorgente incondizionatamente, potete
usarlo per ricaricare un file sorgente che sia stato cambiato in seguito
all'avvio del programma:
5.times do |i| File.open("temp.rb","w") { |f| f.puts "module Temp\ndef Temp.var() #{i}; end\nend" } load "temp.rb" puts Temp.var end |
produce:
0 1 2 3 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.