Včera jsem byl na pohovoru na pozici Ruby vývojáře v jedné firmě tady v Denveru, když mi tazatel položil dobrou metaprogramátorskou výzvu: požádal mě, abych napsal automatický auditor metod.
Popisoval metodu třídy, která by vzala jméno instanční metody a vygenerovala automatickou, zaznamenanou papírovou stopu při každém volání této cílové metody. Něco takového:
To by vytvořilo výstup, který by vypadal takto:
Navrhl jsem řešení, které fungovalo, ale zahrnovalo některé potenciálně nebezpečné zásahy do Object#send
. Před prezentací svého řešení jsem si řekl: „Tohle je asi špatný nápad, ale…“ a měl jsem pravdu. Dobré nápady málokdy takhle začínají.
Řešení, které navrhl, zahrnovalo použití často přehlížené Module#prepend
a já jsem si myslel, že je dostatečně cool na to, aby si zasloužilo zápis.
Strategie spočívala ve využití chování Module#prepend
k vytvoření dynamicky generované obalové metody pro Messenger#share
.
V tomto wrapperu necháme před provedením těla metody vystřelit jednu zprávu „Provádění“, samotné provedení metody a pak závěrečnou zprávu „Ukončení“, jakmile provedení metody úspěšně skončí.
Abychom však pochopili, jak k tomu použít Module#prepend
, potřebujeme nejprve dobře porozumět tomu, jak v Ruby funguje dědičnost metod.
Řetězce předků
Každý objekt Ruby se nachází na konci takzvaného řetězce předků. Je to seznam objektů předků, od kterých objekt dědí. Ruby používá tento Řetězec předků k určení, která verze metody (pokud vůbec nějaká) se provede, když objekt obdrží zprávu.
Strom předků pro libovolný objekt můžete skutečně zobrazit voláním Module#ancestors
. Například zde je řetězec předků pro naši třídu Messenger
:
Když do třídy #include
nebo #prepend
vložíme modul, Ruby provede změny v řetězci předků této třídy a upraví, které metody a v jakém pořadí budou nalezeny. Velký rozdíl mezi #include
a #prepend
je v tom, kde se tato změna provede. Vytvořme modul Auditable
, který bude (nakonec) obsahovat kód, který provádí naše auditovací kouzla:
Pokud použijeme Module#include
pro import (v současnosti neexistujících) metod z Auditable
, Ruby vmáčkne tento modul jako předka naší třídy Messenger
. Zde se přesvědčte sami:
Když zavoláme metodu třídy Messenger
, Ruby se podívá, co je definováno ve třídě Messenger
, a – pokud nenajde metodu, která by odpovídala volání – vyšplhá se po řetězci předků až k Auditable
, aby se podíval znovu. Pokud nenajde to, co hledá, na Auditable
, přejde na Object
a tak dále.
To je #include
. Pokud místo toho použijeme Module#prepend
pro import obsahu modulu, získáme zcela jiný efekt:
#prepend
udělá z Messenger
předka Auditable
. Počkejte. Cože?“
Může to vypadat, že Messenger
je nyní nadtřídou Auditable
, ale to není přesně to, co se zde děje. Instance třídy Messenger
jsou stále instancemi třídy Messenger
, ale Ruby nyní bude hledat metody pro Messenger
v modulu Auditable
dříve, než je bude hledat v samotné třídě Messenger
.
A právě toho, přátelé, využijeme při sestavování tohoto auditora: pokud vytvoříme metodu s názvem Auditable#share
, Ruby ji najde dříve než Messenger#share
. Můžeme pak použít volání super
v Auditable#share
k přístupu (a vykonání!) k původní metodě definované na Messenger
.
Samotný modul
Vlastně nebudeme vytvářet metodu s názvem Auditable#share
. Proč ne? Protože chceme, aby to byl flexibilní nástroj. Pokud natvrdo zakódujeme metodu Auditable#share
, budeme ji moci použít pouze na metody, které implementují metodu #share
. Nebo hůř, museli bychom znovu implementovat stejný vzor auditora pro každou metodu, kterou bychom kdy chtěli auditovat. Ne, díky.
Místo toho tedy budeme naši metodu definovat dynamicky a metodu třídy audit_method
, která ji bude spouštět:
Při volání audit_method
v implementační třídě se v modulu Auditable
vytvoří metoda se stejným názvem jako metoda, kterou chceme auditovat. V našem případě se vytvoří metoda s názvem Auditable#share
. Jak již bylo řečeno, Ruby najde tuto metodu dříve než původní metodu na Messenger
, protože v implementační třídě předřazujeme modul Auditable
.
To znamená, že můžeme použít supervolání, abychom se dostali nahoru v řetězci předků a provedli Messenger#send
. Když to uděláme, předáme shromážděné argumenty (*arguments
) také nahoru v řetězci.
Po zavolání původní metody vypíšeme naši výstupní zprávu a tím to hasne. Dobrá práce, bando!“
Spojení všeho dohromady
Teď už jen stačí prepend
přidat tento modul do naší třídy Messenger
a měli bychom být připraveni:
A světe div se, ono to funguje:
Důsledky pro auditování jsou zde obrovské, ale tento trik není jen tak. Pomocí prependingu můžete měnit chování objektů, aniž byste měnili objekty samotné. Jedná se o přizpůsobivý a přehledný způsob vytváření komponent vyššího řádu v jazyce Ruby. Testování výkonu, zpracování chyb. Můžete toho zde udělat hodně.
Závěr
Moduly v jazyce Ruby jsou složitější, než si většina lidí myslí. Není to tak jednoduché jako „přehazovat“ kód z jednoho místa na druhé a pochopení těchto rozdílů vám odemkne některé opravdu šikovné nástroje pro váš opasek s pomůckami.
Možná jste si všimli, že jsem dnes mluvil pouze o Module#include
a Module#prepend
a že jsem se nedotkl Module#extend
. To proto, že funguje zcela jinak než jeho příbuzní. Brzy sepíšu podrobný výklad o Module#extend
, aby byla sada kompletní.
Prozatím, pokud se chcete dozvědět více, doporučuji přečíst si Ruby modules: Doporučujeme: Include vs Prepend vs Extend od Léonarda Hetsche. Bylo to opravdu užitečné při skládání toho všeho dohromady.