Dynamisk opprette typer basert på XML


tirsdag 30. mars 2010 Ruby XML

Én av fordelene med å programmere i et dynamisk språk er at man kan opprette typer og objekter basert på data som er ukjent i designtime. Som for eksempel å opprette sterkt typede objekter basert på XML. For hvorfor skal jeg jobbe med XML når jeg kan jobbe med objekter?!

Men da jeg begynte å skikke på XML-biblotekene i Ruby ble jeg faktisk litt skuffet.., det fungerte nemlig ikke sånn out-of-the-box. Jeg fant en rekke open source prosjekter som tenkte i samme bane som meg, men kvaliteten var svært varierende. Jeg fikk derfor lyst til å forsøke selv, mest får å trene på meta-programmering.

Jeg har ikke kommet så langt som jeg ønsket enda – denne formen for programmering er krevende, ettersom man har to lag med kode man må holde rede på (den man skriver, og den som oppstår når programmet kjører). Jeg har derimot noe som fungerer godt nok for et lite eksempel, så da er det på tide med en blogpost.

Sett at jeg har XML'en som følger nedenfor. Jeg ønsker å kunne parse denne med en generisk modul som gir meg en typet kolleksjon tilbake.

1 <people>
2   <person category="Programmer">
3     <firstname>Bob</firstname>
4     <lastname>Martin</lastname>
5   </person>
6   <person category="Programmer">
7     <firstname>Kent</firstname>
8     <lastname>Beck</lastname>
9   </person>
10 </people>

For akkurat dette eksempelet vil jeg ha ut en objektstruktur som den nedefor (den kunne vært bedre, men det er dette koden min produserer for øyeblikket). XmlObject opprettes ved å sende inn en xml. Når parsingen er ferdig har objektet fått en people-property. People er en kolleksjon av Person – med en count-propery og en iterator. Person-klassen som er opprettet har fått properties tilsvarende det vi finner i xml'en: category, firstname og lastname.

XmlObject_spec (2)

Her er et eksempel på bruk av XmlObject, tatt fra enhetstestene jeg brukte for å utvikle løsningen.

Utdrag fra xml_object_spec.rb:

28   def setup
29     @xml_obj = XmlObject.new PEOPLE_XML
30   end
---- snip ----
66   def test__usage_sample
67     list_of_names = []
68     @xml_obj.people.each do |person|
69       list_of_names << "#{person.lastname}, #{person.firstname} (#{person.category})"
70     end
71     list_of_names.first.should == "Martin, Bob (Programmer)"
72     list_of_names.last.should == "Beck, Kent (Programmer)"
73   end

PEOPLE_XML er dataene gjengitt i starten av denne artikkelen. Som du ser har @xml_obj etter setup fått en people-property jeg kan bruke til å iterere og hente ut data om personene. I linje 71 og 72 verifiserer jeg at objektene inneholdt det jeg forventet.

En fullstendig kodelisting av løsningen vil forvirre mer enn jeg ønsker.., jeg vil heller trekke frem et par ting som illustrerer hva som er mulig i forhold til in-memory kodegenerering i runtime. Nedenfor ser du for eksempel en metode jeg bruker for å opprette en ny klasse basert på et XML element.

15     def define_class_and_create_instanse name
16       name = name.capitalize
17       unless XmlObject.const_defined? name.to_sym
18         eval %(
19           class #{name} < Array
20             alias :count :length
21           end
22         )
23       end
24       klass = eval "XmlObject::#{name}"
25       [klass, klass.new]
26     end

Metoden tar inn navnet på elementet, og sørger for at det begynner med en stor bokstav (linje 16). Hvis klassen ikke er definert fra før (linje 17) oppretter jeg en ny klasse som arver fra Array ved å evaluere strengen i linje 19 til 21. Alias (linje 20) brukes for å døpe length-propertien til Array om til "count", som er det jeg ønsker objektet skal ha.

I linje 24 bruker jeg eval igjen for å få tak i en referanse til den nye klassen. Til slutt returnerer jeg klassen samt en ny instans (i et array á to elementer – linje 25).

Neste eksempel er metoden jeg bruker for å opprette attributter (properties) og sette verdien for et gitt objekt.

50     def add_attr name, klass, instanse, value
51       attr_name = name.to_sym
52       klass.send(:attr_accessor, attr_name) unless klass.respond_to? attr_name
53       instanse.send "#{attr_name}=".to_sym, value
54     end

I linje 52 sender jeg meldingen "attr_accessor" til klassen – se på det som å kalle en statisk metode som heter attr_accessor. Denne metoden brukes til å opprette en get/set-property (reader/writer accessor i Ruby) – attr_name bestemmer navnet på propertien.

Legg også merke til at kallet til "send" er etterfulgt av en unless-statement. Hvis klassen allerede har denne propertien, ved at den responderer på property-navnet, trenger jeg ikke opprette propertien på nytt.

I linje 53 setter jeg verdien på den nye propertien ved å bruke send-metoden til en instans av klassen. Det jeg egentlig gjør er å kaller setter-metoden for propertien – navnet på den er like navnet på propertien pluss et erlik-tegn (=). Andre parameter til send er verdien som skal settes.

Dette er bare et lite utdrag av de mange metodene og teknikkene man har tilgjengelig for introspection og dynamisk metaprogrammering i Ruby. Jeg håper dette gir et lite innblikk i de enorme mulighetene man har i dynamiske språk som Ruby i forhold til mere statiske språk.

Les også: Du MÅ beherske et dynamsik språk | Hvilket dynamisk programmeirngsspråk du skal lære deg


comments powered by Disqus