Programming Ruby

The Pragmatic Programmer's Guide

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:

  1. Moduli forniscono un namespace (un insieme di nomi, in cui tutti i nomi sono unici) e prevengono i conflitti tra i nomi.
  2. Moduli implementano la capacità del mixin.

Namespaces (un insieme di nomi, in cui tutti i nomi sono unici)

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 .

Mixins

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 include 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.

Instance Variables in Mixins

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 @numNotes. 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).

Iteratori e Modulo Enumerabile

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.

Includendo altri File

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.