Prototyper og properties for Rails
Nylig hadde jeg behov for å kunne konfigurere ActiveRecord-objekter med et system som tillot standardverdier og overstyringer i en slags cascade. Kort tid etter jeg hadde gjort dette leste jeg en artikkel av Steve Yegge som inspirerte meg til en liten refaktorering og samling av koden som en plugin. Resultatet er acts_as_prototype.
Brukseksempel
Mitt konkrete brukseksempel var e-poster. Jeg bruker Liquid til å lagre e-postmaler i databasen slik jeg beskrev i innlegget "Enda litt mer Ruby-templating". Som et eksempel kan vi se for oss et system som har konti, prosjekter og klienter. En konti har mange prosjekter, og et prosjekt har en eller flere klienter.
La oss si at vi ønsker å kunne personalisere løsningen for de som ønsker det. Eksempelvis ønsker man å tilpasse klientenes "glemt passord"-epost som sendes når de, vel, har glemt passordet sitt.
- Systemet har en standardmal for glemt passord-e-poster
- Kontoeier skal kunne overstyre denne for sin konto
- Kontoeier skal kunne overstyre denne for ett enkeltprosjekt
- Kontoeier skal kunne overstyre denne for en enkelt klient
Dette er utgangspunktet som ga meg følgende løsning:
# Sett en property i "det globale skopet"
fpt_global = Property.set :forgot_password_template, EmailTemplate.find(1)
# Sjekk hvilken mal en konto bruker for glemt passord
account = Account.find(1)
account.properties[:forgot_password_template] # => #<EmailTemplate id: 1...>
# Overstyr malen for dette prosjektet
account.properties[:forgot_password_template] = EmailTemplate.find(2)
# Lag et nytt prosjekt som arver properties fra kontoen
project = Account.beget(Project, { :name => "My awesome project" })
project.properties[:forgot_password_template] # => #<EmailTemplate id: 2...>
Da jeg skrev denne koden var jeg opprinnelig inspirert av konfigurasjonssystemet i eZ Publish som har globale settings og flere nivåer med "overrides". Etter å ha lest den nevnte artikkelen til Steve Yegge, som omhandler "the prototype pattern" innså jeg at det var en minimal implementasjon av dette jeg egentlig hadde laget og refaktorerte litt.
Kort sagt er prototype-patternet det samme som JavaScript bruker for sine objekter, så om du kjenner til hvordan properties og prototyper (og -kjeder) virker i JavaScript så har du kjernen i patternet.
Properties fra acts_as_prototype kan lagre de fleste typer verdier. ActiveRecord-objekter (som i eksempelet over) lagres per referanse, omtrent som polymorfe assosiasjoner ellers, mens andre verdier dumpes til et tekstfelt i basen med Marshal.dump.
Installasjon
ruby script/plugin install git://github.com/cjohansen/acts_as_prototype.git
For de som er nøye på det:
cd vendor/plugin/acts_as_prototype
rake test
Og for alle:
ruby script/generate property_migration
rake db:migrate
Dette gir deg to nye tabeller, en for prototyper og en for properties. Deretter kan du bare kjøre makroen acts_as_prototype i en hvilken som helst ActiveRecord-klasse, og du får tre nye metoder:
-
prototype -
properties -
beget(klass, attributes)
Noe mer beskrivelse finner du på Github.
Planer
Først og fremst har jeg planer om å fortsette å teste dette litt, finne flere bruksområder, og få en følelse av om det er noe å satse videre på. Om andre prøver og har innspill er jeg veldig interessert i dem.
Den eneste konkrete planen jeg foreløpig sitter med er å implementere noe caching/memoisering på et nivå for å få ned antall spørringer mot databasen. Jeg har gjort ett forsøk på dette, men det er litt klønete å unngå at cachen blir "stale" pga prototypekjedene. Du vet aldri hvilket objekt som egentlig sitter på propertien for et gitt objekt, og dette gjør det litt vrient å cache. Men jeg har en idé som jeg skal prøve ved anledning.
I mellomtiden er jeg veldig interessert i innspill/idéer!