Programmering i Ruby

Den Pragmatiske Programmerers Veiledning

Forrige < Innhold ^
Neste >

Moduler



Moduler er en måte å gruppere sammen metoder, klasser og konstanter. Moduler gir deg to hoved-fordeler:
  1. Moduler gir deg et navnerom og forhindrer navnekollisjoner.
  2. Moduler implementerer mixin-egenskapen.

Navnerom

Når du starter med å skrive større og større Ruby-programmer, vil du naturlig finne at du produserer deler av gjenbrukbar kode - biblioteker med relaterte rutiner som er generelt anvendelige. Du kan da ønske å legge denne koden inn i separate filer slik at innholdet kan bli delt mellom flere Ruby-programmer.

Ofte vil denne koden være organisert i klasser, slik at du vil antagelig putte en klasse (eller et sett av relaterte klasser) i en fil.

Imidlertid er det også ganger hvor du vil gruppere ting i sammen som ikke naturlig former en klasse.

En initiell tilnærming kan være å putte alle disse tingene inn i en fil og ganske enkelt laste inn den filen til alle programmer som trenger denne. Dette er hvordan C-språket fungerer. Men det er et problem. Si at du skriver et trigonometrisk sett av funksjoner sin, cos, og så videre. Du kan putte dem alle inn i en fil, trig.rb, slik at framtidige generasjoner kan nyte den. I mellomtiden jobber Sally på en simulering av det gode og det onde, og lager kode-sett med hennes egne nyttige rutiner, inkludert beGood og sin, og legger dem i action.rb. Joe, som vil skrive et program som viser hvor mange engler som kan danse på et knappenålshode, trenger å laste både trig.rb og action.rb inn i sitt program. Men begge definerer en metode som kalles sin. Dårlige nyheter.

Svaret er modul-mekanismen. Moduler definerer et navnerom (namespace), en sandkasse hvor dine metoder og konstanter kan leke uten å trenge å være bekymret for å bli trampet på av andre metoder og konstanter. Trig-funksjonene kan gå inn i en modul:

module Trig
  PI = 3.141592654
  def Trig.sin(x)
   # ..
  end
  def Trig.cos(x)
   # ..
  end
end

og de gode og dårlige action-metodene kan fungere med hverandre:

module Action
  VERY_BAD = 0
  BAD      = 1
  def Action.sin(badness)
    # ...
  end
end

Modul-konstanter er navngitt som klasse-konstanter med en initiell stor bokstav. Metode-definisjonene ser like ut også: disse modul-metodene er definert akkurat som klasse-metoder.

Hvis et tredje program vil benytte disse modulene, kan det ganske enkelt laste opp de to filene(ved å bruke Rubys require utsagn, som diskuteres på side 103) og referere til de kvalifiserte navnene.

require "trig"
require "action"

y = Trig.sin(Trig::PI/4) wrongdoing = Action.sin(Action::VERY_BAD)

Som med klasse-metodene kan du kalle en modul-metode ved å sette foran modulnavnet og et punktum, og du kan referere en konstant ved å benytte modulnavnet og to kolon.

Mixins

Moduler har en annen, meget bra anvendelse. På et slag kan de så og si eliminere behovet for multippel arv, og tilbyr en fasilitet som kalles mixin.

I de foregående seksjonenes eksempler definerte vi modul-metoder, metoder hvor navnene var prefikset med modul-navnet. Hvis dette fikk deg til å tenke på klasse-metoder, ville kanskje din neste tanke være: "Hva skjer hvis jeg definerer instansmetoder inne i en modul?". Godt spørsmål. En modul kan ikke ha instanser, fordi en modul er ikke en klasse. Men likevel, du kan inkludere en modul med en klasse-definisjon. Når dette skjer vil alle modulens instansmetoder bli plutselig tilgjengelig som metoder i klassen også. De blir mikset inn. Faktisk så opptrer mixed-in moduler effektivt som super-klasser.

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 (#537762134): West End Blues"
et.whoAmI? » "EightTrack (#537761824): Surrealistic Pillow"

Ved å inkludere Debug-modulen, vil både Phonograph og EightTrack få tilgang til whoAmI? instansmetoden.

Et par punkter angående include-utsagnet før vi går videre. Først det har ingenting å gjøre med filer. C-programmmerere bruker et preprosessor-direktiv kalt #include for å legge inn innholdet i en fil inn i en annen under kompilering. Rubys include -utsagn lager ganske enkelt en referanse til en navngitt modul. Hvis den modulen er i en separat fil, må du benytte require for å dra denne filen inn før du bruker include. For det andre, et Ruby include kopierer ikke bare modulens instansmetoder inn i klassen. Isteden så lages det en referanse fra klassen til den inkluderte modulen. Hvis multiple klasser inkluderer den modulen, vil de alle peke til den samme tingen. Hvis du forandrer definisjonen på en metode innen en modul, selv mens programmet kjører, vil alle klassene som inkluderer den modulen utføre den nye oppførselen.[Selvfølgelig snakker vi kun om metoder her. Instansvariabler er alltid per-objekt, for eksempel.]

Mixins gir deg en flott, kontrollert måte å gi funksjonalitet til klasser. I midlertid kommer deres virkelige styrke når koden i mixinen starter å interagere med kode i klassen som benytter den. La oss ta en standard Ruby-mixin Comparable som eksempel. Comparable-mixinen kan bli benyttet til å legge til sammenlignings-operatorer (<, <=, ==, >=, and >), så vel som metoden between?, til en klasse. For å få dette til å fungere forutsetter Comparable at alle klasser som bruker den definerer operatoren <=>. Og da som en klasse-skribent, vil du definere en metode <=>, inkludere Comparable,og få 6 sammenlignings-funksjoner gratis. La oss prøve det med vår Song-klasse ved å gjøre sangene sammenlignbare basert på deres varighet. Alt vi må gjøre er å inkludere Comparable-modulen og implementere sammenlignings-operatoren <=>.

class Song
  include Comparable
  def <=>(other)
    self.duration <=> other.duration
  end
end

Vi kan sjekke om resultatene er fornuftige med et par test-sanger.

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

Så til slutt, tilbake på side 43, viste vi en implementering av Smalltalks inject-funksjon, implementerte den innen klassen Array. Vi lovte da å gjøre det mere generelt brukbart. Hvilken måte bedre da en å gjøre det til en mixin-modul?

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

Vi kan så teste dette ved å mikse det inn i noen innebygde klasser.

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"

For et mere utførlig eksempel på en mixin, se på dokumentasjonen for Enumerable -modulen, som starter på side 403.

Instansvariabler i Mixins

Mange som kommer til Ruby fra C++ spør oss ofte, "Hva skjer med instans variabler i en mixin? I C++ må man gjøre seg mye bry for å kontrollere hvordan variabler blir delt i et multippel-arv hierarki. Hvordan håndterer Ruby dette?"

Vel, for å begynne. Det er ikke egentlig noe rettferdig spørsmål, sier vi. Husk hvordan instansvariabler arbeider i Ruby: den første benevningen av en "@"-prefikset variabel, skaper instansvariabelen i det gjeldende objektet, self.

For en mixin, betyr dette at modulen du mikser inn i din klient-klasse kan skape instansvariabler i klient-objektet og kan bruke attr og venner for å definere accessors for disse instansvariablene. For eksempel:

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)
produserer:
Instance method returns 434.7
Instance variable is 434.7

Ikke bare har vi tilgang til metodene definert i mixinen, men vi får også tilgang til de nødvendige instansvariablene. Det er en risiko her, selvfølgelig, at forskjellige mixins kan bruke en instansvariabel med samme navn og skape en kollisjon.

module MajorScales
  def majorNum
    @numNotes = 7 if @numNotes.nil?
    @numNotes # Return 7
  end
end

module PentatonicScales   def pentaNum     @numNotes = 5 if @numNotes.nil?     @numNotes # Return 5?   end end

class ScaleDemo   include MajorScales   include PentatonicScales   def initialize     puts majorNum # Should be 7     puts pentaNum # Should be 5   end end

ScaleDemo.new
produserer:
7
7

De to bitene av kode som vi mikser inn bruker en instansvariabel kalt @numNotes. Uheldigvis var resultatet antagelig ikke hva forfatteren ville.

For det meste prøver ikke mixin-moduler å bære sine egne instansdata rundt---de bruker accessors for å få tak i data fra klient-objektet. Men hvis du trenger en mixin som trenger å ha sin egen tilstand, forsikre deg om at instansvariabelene har unike navn for å skille dem fra andre mixins i systemet(kanskje ved å bruke modulens navn som en del av variabel-navnet).

Iteratorer og Enumerable-modulen

Du har antagelig sett at Ruby-samlingen av klasser støtter et stort antall av operasjoner som kan gjøre forskjellige ting med samlingen: traversere, sortere, og så videre. Du tenker kanskje "Det ville jammen vært fint hvis min klasse kunne støtte alle disse flotte egenskapene også!".

Vel, dine klasser kan støtte alle disse flotte egenskapene basert på magien til mixins og modulen Enumerable. Alt du trenger å gjøre er å skrive en iterator kalt each, som returnerer alle elementer i din samling etter tur. Mix inn Enumerable, og plutselig støtter din klasse slike ting som map,include?, og find_all?. Hvis objektene i din samling implementerer en meningsfull ordering semantikk ved å bruke <=>-metoden, vil du også få min, max, og sort.

Inkludere andre filer

Fordi Ruby gjør det enkelt å skrive god, modulær kode, vil du ofte finne deg selv produsere små filer som inneholder en liten bit av selv-drevet funksjonalitet---et brukergrensesnitt til x, en algoritme for å gjøre y, og så videre. Typisk, vil du organisere disse filene som klasse eller modul-biblioteker.

Etter å ha produsert filene vil du innlemme dem i dine nye programmer. Ruby har to utsagn som gjør dette:

load "filename.rb"

require "filename"

load-metoden inkluderer den navngitte Ruby-kildefilen hver gang metoden blir eksekvert, mens require laster en hvilken som helst gitt fil bare en gang. require har tilleggs-funksjonalitet: den kan laste delte binære biblioteker. Begge rutiner aksepterer relative og absolutte stier. Hvis gitt en relativ sti (eller bare et enkelt navn), vil de søke hver katalog i nåværende sti ($:, diskutert på side 140) for filen.

Filer som lastes ved å bruke load og require kan selvfølgelig laste andre filer, som inkluderer andre filer og så videre. Hva som ikke ser åpenbart ut er at require er et eksekverbart utsagn---de kan være inn i et if- utsagn, eller det kan inkludere en streng som akkurat ble laget. Søkestien kan bli endret under kjøring også. Bare legg til den katalogen du vil til strengen $:.

Siden load vil inkludere kildekoden betingelsesløst, kan du bruke det til å innlaste en kildefil som kan ha forandret seg siden programmet startet:

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
produserer:
0
1
2
3
4


( In progress translation to Norwegian by NorwayRUG. $Revision: 1.8 $ )
$Log: tut_modules.xml,v $
Revision 1.8  2002/07/23 13:07:51  kent
alltt på log.

Revision 1.7  2002/07/16 12:33:51  kent
Kjapp gjennomgang. Fikset "supportere", "instanse-" og annet smårask.


Forrige < Innhold ^
Neste >

Extracted from the book "Programming Ruby - The Pragmatic Programmer's Guide".
Translation to norwegian by Norway Ruby User Group.
Copyright for the english original authored by David Thomas and Andrew Hunt:
Copyright © 2001 Addison Wesley Longman, Inc.
This material may be distributed only subject to the terms and conditions set forth in the Open Publication License, v1.0 or later (the latest version is presently available at
http://www.opencontent.org/openpub/).

(Please note that the license for the original has changed from the above. The above is the license of the original version that was used as a foundation for the translation efforts.)

Copyright for the norwegian translation:
Copyright © 2002 Norway Ruby User Group.
This material may be distributed only subject to the terms and conditions set forth in the Open Publication License, v1.0 or later (the latest version is presently available at
http://www.opencontent.org/openpub/).
Distribution of substantively modified versions of this document is prohibited without the explicit permission of the copyright holder.
Distribution of the work or derivative of the work in any standard (paper) book form is prohibited unless prior permission is obtained from the copyright holder.