Eilen olin haastattelemassa Ruby-kehittäjän paikkaa eräässä yrityksessä täällä Denverissä, kun haastateltavani esitti hyvän metaohjelmointihaasteen: hän pyysi minua kirjoittamaan automatisoidun metodien auditoijan.
Hän kuvasi luokan metodin, joka ottaisi instanssimetodin nimen ja loisi automaattisen, kirjautuneen paperikirjan joka kerta, kun kyseistä kohdemetodia kutsutaan. Jotain tällaista:
Se tuottaisi tulosteen, joka näyttäisi tältä:
Keksin ratkaisun, joka toimi, mutta sisälsi joitakin potentiaalisesti vaarallisia hakkerointeja Object#send
. Ennen kuin esittelin ratkaisuni, sanoin ”tämä on luultavasti huono idea, mutta…” ja olin oikeassa. Hyvät ideat alkavat harvoin näin.
Ratkaisu, jota hän ehdotti, sisälsi usein unohdetun Module#prepend
:n käyttämisen, ja minusta se oli tarpeeksi siisti ansaitakseen kirjoituksen.
Strategia oli käyttää Module#prepend
:n käyttäytymistä luodakseen dynaamisesti generoidun wrapper-metodin Messenger#share
:lle.
Tässä wrapperissa meillä on yksi ”Performing”-viesti, joka laukeaa ennen metodin rungon suorittamista, itse metodin suoritus ja sitten viimeinen ”Exiting”-viesti, kun metodin suoritus on suoritettu onnistuneesti.
Mutta jotta ymmärtäisimme, miten voimme käyttää Module#prepend
:ää tähän, tarvitsemme ensin hyvän käsityksen siitä, miten metodien periytyminen toimii Rubyssä.
Ancestor Chains
Jokainen Ruby-objekti sijaitsee niin sanotun Ancestor Chainin päässä. Se on luettelo esi-objekteista, joista objekti perii. Ruby käyttää tätä esi-isäketjua määrittämään, mikä versio metodista (jos sellainen ylipäätään on) suoritetaan, kun objekti vastaanottaa viestin.
Voit itse asiassa tarkastella minkä tahansa objektin esi-isäpuuta kutsumalla Module#ancestors
. Tässä on esimerkiksi Messenger
-luokkamme esivanhempien ketju:
Kun #include
tai #prepend
-moduuli tulee luokkaan, Ruby tekee muutoksia kyseisen luokan esivanhempien ketjuun ja säätää, mitkä metodit löytyvät ja missä järjestyksessä. Suuri ero #include
:n ja #prepend
:n välillä on se, missä tämä muutos tehdään. Luodaan moduuli nimeltä Auditable
, joka (lopulta) sisältää koodin, joka tekee auditointitaikamme:
Jos käytämme Module#include
tuodaksemme (tällä hetkellä olemattomat) metodit luokasta Auditable
, Ruby puristaa tuon moduulin luokkamme Messenger
esi-isäksi. Tässä, katso itse:
Kun kutsumme metodia Messenger
:ssa, Ruby katsoo, mitä Messenger
-luokassa on määritelty, ja – jos se ei löydä kutsua vastaavaa metodia – kiipeää ylöspäin esi-isäketjussa Auditable
:een etsimään uudelleen. Jos se ei löydä etsimäänsä Auditable
:stä, se siirtyy Object
:een ja niin edelleen.
Tässä on #include
. Jos sen sijaan käytämme Module#prepend
tuodaksemme moduulin sisällön, saamme täysin erilaisen vaikutuksen:
#prepend
tekee Messenger
:sta Auditable
:n esi-isän. Odota. Mitä?
Tämä saattaa näyttää siltä, että Messenger
on nyt Auditable
:n yläluokka, mutta tässä ei tapahdu aivan niin. Luokan Messenger
instanssit ovat edelleen luokan Messenger
instansseja, mutta Ruby etsii nyt Messenger
:lle käytettäviä metodeja Auditable
-moduulista ennen kuin etsii niitä itse Messenger
-luokasta.
Ja tätä, ystävät, hyödynnämme rakentaessamme tämän auditorin: jos luomme metodin nimeltä Auditable#share
, Ruby löytää sen ennen kuin se löytää Messenger#share
. Voimme sitten käyttää super
-kutsua Auditable#share
:ssä päästäksemme käsiksi (ja suorittaaksemme!) alkuperäiseen metodiin, joka on määritelty osoitteessa Messenger
.
Moduuli itse
Me emme oikeastaan aio luoda metodia nimeltä Auditable#share
. Miksi emme? Koska haluamme tämän olevan joustava apuohjelma. Jos koodaamme metodin Auditable#share
, voimme käyttää sitä vain metodeissa, jotka toteuttavat metodin #share
. Tai mikä vielä pahempaa, meidän täytyisi toteuttaa tämä sama auditointimalli uudelleen jokaiselle metodille, jonka haluamme auditoida. Ei kiitos.
Sen sijaan määrittelemme metodimme dynaamisesti ja audit_method
-luokan metodin, jolla se laukaistaan:
Kun audit_method
kutsutaan toteuttavassa luokassa, Auditable
-moduuliin luodaan metodi, jolla on sama nimi kuin tarkastettavalla metodilla. Meidän tapauksessamme se luo metodin nimeltä Auditable#share
. Kuten mainittiin, Ruby löytää tämän metodin ennen kuin se löytää alkuperäisen metodin Messenger
, koska prependoimme Auditable
-moduulin toteuttavassa luokassa.
Tämä tarkoittaa, että voimme käyttää super-kutsua päästäksemme ylöspäin esi-isäketjussa ja suorittaa Messenger#send
. Näin tehdessämme välitämme myös keräämämme argumentit (*arguments
) ylöspäin ketjussa.
Kun olemme kutsuneet alkuperäistä metodia, tulostamme poistumisviestin ja lopetamme. Hyvää työtä, jengi!
Yhteenveto
Nyt on vain prepend
lisättävä tämä moduuli Messenger
-luokkaamme, ja meidän pitäisi olla valmiina:
Ja hyvänen aika, se toimii:
Vaikutukset ovat valtavat auditoinnin kannalta,
mutta tässä tempauksessa on muutakin. Voit käyttää prependingiä objektien käyttäytymisen muuttamiseen muuttamatta itse objekteja. Tämä on mukautuva ja selkeä tapa luoda korkeamman asteen komponentteja Rubyssä. Suorituskyvyn testaus, virheiden käsittely. Tässä voi tehdä paljon.
Johtopäätös
Rubyn moduulit ovat monimutkaisempia kuin useimmat luulevat. Se ei ole niin yksinkertaista kuin koodin ”dumppaaminen” paikasta toiseen, ja näiden erojen ymmärtäminen avaa joitakin todella siistejä työkaluja apuvyöhön.
Olet ehkä huomannut, että puhuin tänään vain Module#include
:stä ja Module#prepend
:stä, enkä koskenut Module#extend
:aan. Tämä johtuu siitä, että se toimii hyvin eri tavalla kuin sen serkut. Kirjoitan pian syvällisen selityksen Module#extend
:sta täydentääkseni sarjaa.
Nyt jos haluat oppia lisää, suosittelen lukemaan Rubyn moduulit: Include vs Prepend vs Extend by Léonard Hetsch. Siitä oli todella paljon apua tämän kaiken kokoamisessa.