Tegnap egy Ruby fejlesztői pozícióra voltam interjún egy denveri cégnél, amikor az interjúztatóm egy jó metaprogramozási kihívást tett fel: megkért, hogy írjak egy automatikus metódusellenőrzést.
Leírt egy osztály metódust, amely egy példány metódus nevét veszi, és egy automatikus, naplózott papírnyomot generál minden alkalommal, amikor a célmetódus meghívásra kerül. Valami ilyesmit:
Ez olyan kimenetet eredményezne, ami így nézne ki:
Kitaláltam egy megoldást, ami működött, de a Object#send
néhány potenciálisan veszélyes hackeléssel járt. Mielőtt bemutattam volna a megoldásomat, azt mondtam, hogy “ez valószínűleg rossz ötlet, de…”, és igazam volt. A jó ötletek ritkán indulnak így.
A megoldás, amit javasolt, a gyakran figyelmen kívül hagyott Module#prepend
felhasználását tartalmazta, és úgy gondoltam, hogy elég menő ahhoz, hogy megérdemeljen egy írást.
A stratégia az volt, hogy a Module#prepend
viselkedését felhasználva dinamikusan generált wrapper metódust hozunk létre a Messenger#share
számára.
Ebben a wrapperben lesz egy “Performing” üzenet, ami a metódus testének végrehajtása előtt lő ki, maga a metódus végrehajtása, majd egy végső “Exiting” üzenet, miután a metódus végrehajtása sikeresen befejeződött.
De ahhoz, hogy megértsük, hogyan használjuk a Module#prepend
-t erre a célra, először is jól meg kell értenünk, hogyan működik a metódus öröklődés a Rubyban.
Az ősláncok
Minden Ruby objektum egy úgynevezett őslánc végén helyezkedik el. Ez azoknak az ősobjektumoknak a listája, amelyektől az objektum örököl. A Ruby ezt az ősláncot használja arra, hogy meghatározza, hogy egy metódus melyik verziója (ha egyáltalán van ilyen) kerül végrehajtásra, amikor az objektum üzenetet kap.
Az ősfát bármely objektum esetében ténylegesen megtekinthetjük a Module#ancestors
hívásával. Itt van például a Messenger
osztályunk őslánca:
Amikor #include
vagy #prepend
modult illesztünk egy osztályba, a Ruby módosítja az osztályok ősláncát, finomítva, hogy mely metódusokat és milyen sorrendben találjuk meg. A #include
és #prepend
közötti nagy különbség az, hogy hol történik ez a változtatás. Hozzunk létre egy Auditable
nevű modult, amely (végül) azt a kódot fogja tartalmazni, amely az auditálási varázslatunkat végzi:
Ha a Module#include
segítségével importáljuk a (jelenleg nem létező) metódusokat a Auditable
-ből, a Ruby ezt a modult a Messenger
osztályunk őseként fogja bepréselni. Tessék, nézd meg magad:
Amikor meghívunk egy metódust a Messenger
-ban, a Ruby megnézi, hogy mi van definiálva a Messenger
osztályban, és – ha nem talál a hívásnak megfelelő metódust – felmászik az ősláncban a Auditable
-ig, hogy újra megnézze. Ha a Auditable
-nél nem találja meg, amit keres, akkor továbbmegy a Object
-re és így tovább.
Ez a #include
. Ha ehelyett a Module#prepend
-t használjuk a modul tartalmának importálására, teljesen más hatást érünk el:
#prepend
a Messenger
-t a Auditable
ősévé teszi. Várjunk csak! Mi?
Ez úgy tűnhet, mintha a Messenger
mostantól a Auditable
szuperosztálya lenne, de itt nem pontosan ez történik. A Messenger
osztály példányai még mindig a Messenger
osztály példányai, de a Ruby mostantól a Auditable
modulban fogja keresni a Messenger
-hoz használható metódusokat, mielőtt magában a Messenger
osztályban keresné őket.
És ez az, barátaim, amit ki fogunk használni ennek az auditornak a felépítéséhez: ha létrehozunk egy Auditable#share
nevű metódust, a Ruby ezt fogja megtalálni, mielőtt megtalálja a Messenger#share
-et. Ezután egy super
hívást használhatunk a Auditable#share
-ban, hogy elérjük (és futtassuk!) a Messenger
-ban definiált eredeti metódust.
A modul maga
Ténylegesen nem fogunk létrehozni egy Auditable#share
nevű metódust. Miért nem? Mert azt akarjuk, hogy ez egy rugalmas segédprogram legyen. Ha keményen kódoljuk a Auditable#share
metódust, akkor csak olyan metódusokra tudjuk majd használni, amelyek implementálják a #share
metódust. Vagy ami még rosszabb, újra kellene implementálnunk ugyanazt az auditor mintát minden olyan metódushoz, amelyet valaha is auditálni szeretnénk. Nem, köszönöm.
Ehelyett tehát dinamikusan fogjuk definiálni a metódusunkat, és a audit_method
osztály metódusát fogjuk kilőni:
Amikor a audit_method
-t meghívjuk egy implementáló osztályban, a Auditable
modulban létrejön egy metódus, amelynek neve megegyezik az auditálandó metódus nevével. Esetünkben egy Auditable#share
nevű metódust hoz létre. Mint említettük, a Ruby előbb fogja megtalálni ezt a metódust, mint az eredeti Messenger
metódust, mivel az implementáló osztályban a Auditable
modult előlegezzük meg.
Ez azt jelenti, hogy egy szuperhívással feljebb juthatunk az ősláncban, és végre tudjuk hajtani a Messenger#send
metódust. Amikor ezt megtesszük, az összegyűjtött argumentumokat (*arguments
) is továbbítjuk felfelé a láncban.
Amikor meghívtuk az eredeti metódust, kiírjuk a kilépési üzenetünket, és végeztünk. Szép munka, banda!
Az egészet összerakva
Most már csak prepend
kell ezt a modult a Messenger
osztályunkba illeszteni, és máris készen állunk:
És a jó ég áldja meg, hogy működik:
Az itteni következmények óriásiak az auditálás szempontjából, de ez a trükk még többről szól. A prependinget használhatod az objektumok viselkedésének megváltoztatására anélkül, hogy magukat az objektumokat megváltoztatnád. Ez egy adaptálható és egyértelmű módja annak, hogy magasabb rendű komponenseket hozzunk létre Rubyban. Teljesítménytesztelés, hibakezelés. Sok mindent megtehetsz itt.
Következtetés
A Ruby modulok bonyolultabbak, mint azt a legtöbben gondolják. Nem olyan egyszerű, mint a kód “dömpingelése” egyik helyről a másikra, és ezeknek a különbségeknek a megértése néhány igazán ügyes eszközt nyit meg a segédeszköz-öved számára.
Megfigyelhetted, hogy ma csak a Module#include
-ről és a Module#prepend
-ről beszéltem, és hogy a Module#extend
-hoz nem nyúltam. Ez azért van, mert nagyon másképp működik, mint az unokatestvérei. Hamarosan írok egy részletes magyarázatot a Module#extend
-ról, hogy teljes legyen a készlet.
Előre, ha többet szeretnél megtudni, ajánlom a Ruby modulok elolvasását: Include vs Prepend vs Extend című könyvét Léonard Hetsch-től. Ez nagyon hasznos volt abban, hogy mindezt összerakjam.