Často počujete, že metaprogramovanie je niečo, čo používajú iba ruby ninjovia, a že to jednoducho nie je pre bežných smrteľníkov. Pravdou však je, že metaprogramovanie nie je vôbec nič strašidelné. Tento príspevok na blogu bude slúžiť na spochybnenie tohto typu myslenia a priblíženie metaprogramovania priemernému vývojárovi Ruby, aby mohli využívať aj jeho výhody.
Ruby Metaprogramming: Code Writing Code TweetJe potrebné poznamenať, že metaprogramovanie môže znamenať veľa a môže byť často veľmi zneužité a dochádzať do extrému, pokiaľ ide o použitie, takže sa pokúsim uviesť niektoré príklady z reálneho sveta, ktoré by každý mohol použiť pri každodennom programovaní.
Metaprogramovanie je technika, pomocou ktorej môžete napísať kód píše kód sám o sebe dynamicky za behu. To znamená, že môžete definovať metódy a triedy počas behu programu. Šialené, že? Stručne povedané, pomocou metaprogramovania môžete znovu otvárať a upravovať triedy, chytať neexistujúce metódy a vytvárať ich za behu, vytvárať kód, ktorý je SUCHÉ vyhýbaním sa opakovaniu atď.
Predtým, ako sa ponoríme do seriózneho metaprogramovania, musíme preskúmať základné informácie. A najlepšie to urobíte príkladom. Začnime jedným a pochopíme metaprogramovanie Ruby krok za krokom. Pravdepodobne tušíte, čo tento kód robí:
class Developer def self.backend 'I am backend developer' end def frontend 'I am frontend developer' end end
Triedu sme definovali dvoma metódami. Prvá metóda v tejto triede je triedna metóda a druhá je inštančná metóda. Toto je základná vec v Ruby, ale za týmto kódom sa deje oveľa viac, čomu musíme porozumieť, než budeme pokračovať. Za zmienku stojí, že trieda Developer
sama osebe je v skutočnosti predmetom. V Ruby je všetko objekt, vrátane tried. Od Developer
je inštancia, je to inštancia triedy Class
. Takto vyzerá objektový model Ruby:
p Developer.class # Class p Class.superclass # Module p Module.superclass # Object p Object.superclass # BasicObject
Jedna dôležitá vec, ktorú je potrebné pochopiť, je význam self
. The frontend
metóda je bežná metóda, ktorá je k dispozícii v inštanciách triedy Developer
, ale prečo je backend
metóda triedna metóda? Každá časť kódu vykonaná v Ruby je vykonaná proti konkrétnemu ja . Keď tlmočník Ruby vykoná akýkoľvek kód, vždy sleduje hodnotu self
pre ktorýkoľvek daný riadok. self
vždy odkazuje na nejaký objekt, ale tento objekt sa môže meniť na základe vykonaného kódu. Napríklad vo vnútri definície triedy self
odkazuje na samotnú triedu, ktorá je inštanciou triedy Class
.
class Developer p self end # Developer
Metódy vnútornej inštancie, self
odkazuje na inštanciu triedy.
class Developer def frontend self end end p Developer.new.frontend # #
Metódy vnútornej triedy, self
odkazuje na triedu samotnú spôsobom (ktorý bude podrobnejšie rozobraný ďalej v tomto článku):
class Developer def self.backend self end end p Developer.backend # Developer
To je v poriadku, ale čo je nakoniec triedna metóda? Predtým, ako odpovieme na túto otázku, musíme spomenúť existenciu niečoho, čo sa nazýva metaclass, známe tiež ako singleton class a eigenclass. Metóda triedy frontend
, ktoré sme definovali skôr, nie je nič iné ako inštančná metóda definovaná v metaclass pre objekt Developer
! Metaklasa je v podstate trieda, ktorú Ruby vytvára a vkladá do hierarchie dedičstva, aby obsahovala metódy triedy, čím nezasahuje do inštancií vytvorených z triedy.
Každý objekt v Ruby má svoje vlastné metaklasa . Pre vývojára je akosi neviditeľný, ale je tu a môžete ho veľmi ľahko použiť. Od našej triedy Developer
je v podstate objekt, má svoju vlastnú metaklasu. Ako príklad vytvorme objekt triedy String
a manipulovať s jeho metaklasou:
example = 'I'm a string object' def example.something self.upcase end p example.something # I'M A STRING OBJECT
To, čo sme tu urobili, je pridanie singletonovej metódy something
k objektu. Rozdiel medzi metódami triedy a metódami singleton je v tom, že metódy triedy sú dostupné pre všetky inštancie objektu triedy, zatiaľ čo metódy singleton sú dostupné iba pre túto jednu inštanciu. Metódy triedy sú široko používané, zatiaľ čo metódy singleton nie toľko, ale oba typy metód sa pridávajú do metaklasy tohto objektu.
Predchádzajúci príklad by sa dal prepísať takto:
example = 'I'm a string object' class << example def something self.upcase end end
Syntax je iná, ale robí vlastne to isté. Teraz sa vráťme k predchádzajúcemu príkladu, kde sme vytvorili Developer
triedy a preskúmajte niektoré ďalšie syntaxe na definovanie metódy triedy:
class Developer def self.backend 'I am backend developer' end end
Toto je základná definícia, ktorú používa takmer každý.
def Developer.backend 'I am backend developer' end
Toto je to isté, definujeme backend
metóda triedy pre Developer
. Nepoužívali sme self
ale definovanie takejto metódy z nej robí efektívnu triednu metódu.
class Developer class << self def backend 'I am backend developer' end end end
Opäť definujeme metódu triedy, ale pomocou syntaxe podobnej tej, ktorú sme použili na definovanie metódy singleton pre a String
objekt. Môžete si všimnúť, že sme použili self
tu odkazuje na Developer
samotný objekt. Najprv sme otvorili Developer
triedy, čím sa rovná sebe Developer
trieda. Ďalej urobíme class << self
, čím sa budeme rovnať metaclassu Developer
. Potom definujeme metódu backend
na metaklase Developer
.
class << Developer def backend 'I am backend developer' end end
Definovaním takého bloku nastavujeme self
do metriky pre Developer
počas trvania bloku. Vo výsledku bude backend
metóda sa pridáva do metaklasy Developer
, a nie do samotnej triedy.
Pozrime sa, ako sa táto metaklasa správa v strome dedičstva:
Ako ste videli v predchádzajúcich príkladoch, neexistuje žiadny skutočný dôkaz o tom, že metaklasa vôbec existuje. Môžeme však použiť malý hack, ktorý nám môže ukázať existenciu tejto neviditeľnej triedy:
class Object def metaclass_example class << self self end end end
Ak definujeme inštančnú metódu v Object
triedy (áno, môžeme kedykoľvek znovu otvoriť ktorúkoľvek triedu, to je ďalšia krása metaprogramovania), budeme mať self
s odkazom na Object
objekt v ňom. Potom môžeme použiť class << self
syntax na zmenu aktuálneho ja smerovať na metaklasu aktuálneho objektu. Pretože aktuálny objekt je Object
samotná trieda by to bola metaklasa inštancie. Metóda vráti self
čo je v tomto okamihu samotná metaklasa. Takže volaním tejto inštančnej metódy na ľubovoľný objekt môžeme získať metaklasu tohto objektu. Definujme si Developer
triedy a začnite trochu skúmať:
class Developer def frontend p 'inside instance method, self is: ' + self.to_s end class << self def backend p 'inside class method, self is: ' + self.to_s end end end developer = Developer.new developer.frontend # 'inside instance method, self is: #' Developer.backend # 'inside class method, self is: Developer' p 'inside metaclass, self is: ' + developer.metaclass_example.to_s # 'inside metaclass, self is: #'
A čo sa týka crescenda, pozrime sa na dôkaz, že frontend
je inštančná metóda triedy a backend
je inštančná metóda metaklasy:
p developer.class.instance_methods false # [:frontend] p developer.class.metaclass_example.instance_methods false # [:backend]
Ak však chcete získať metaklasu, nemusíte znova otvárať Object
a pridať tento hack. Môžete použiť singleton_class
ktoré poskytuje Ruby. Je to rovnaké ako metaclass_example
pridali sme, ale s týmto hackom môžete skutočne vidieť, ako Ruby funguje pod kapotou:
p developer.class.singleton_class.instance_methods false # [:backend]
Existuje ešte jeden spôsob, ako vytvoriť triednu metódu, a to pomocou instance_eval
:
class Developer end Developer.instance_eval do p 'instance_eval - self is: ' + self.to_s def backend p 'inside a method self is: ' + self.to_s end end # 'instance_eval - self is: Developer' Developer.backend # 'inside a method self is: Developer'
Tento kúsok kódu interpretuje Ruby interpret v kontexte inštancie, ktorá je v tomto prípade a Developer
objekt. A keď definujete metódu na objekte, vytvárate buď metódu triedy alebo metódu singleton. V tomto prípade ide o triednu metódu - presnejšie, triedne metódy sú singletonové metódy, ale singletonové metódy triedy, zatiaľ čo ostatné sú singletonové metódy objektu.
Na druhej strane, class_eval
vyhodnotí kód v kontexte triedy namiesto inštancie. Prakticky to znova otvára triedu. Takto class_eval
možno použiť na vytvorenie inštančnej metódy:
Developer.class_eval do p 'class_eval - self is: ' + self.to_s def frontend p 'inside a method self is: ' + self.to_s end end # 'class_eval - self is: Developer' p developer = Developer.new # # developer.frontend # 'inside a method self is: #'
Stručne povedané, keď voláte class_eval
spôsob, zmeníte self
odkazovať na pôvodnú triedu a keď voláte instance_eval
, self
zmeny odkazujúce na metaklasu pôvodnej triedy.
Ďalšou časťou skladačky metaprogramovania je method_missing
. Keď zavoláte metódu na objekte, Ruby najskôr prejde do triedy a prehľadá jej inštančné metódy. Ak tam nenájde metódu, pokračuje v prehľadávaní reťazca predkov. Ak Ruby metódu stále nenájde, zavolá inú metódu s názvom method_missing
čo je inštančná metóda Kernel
že každý predmet dedí. Pretože sme si istí, že Ruby nakoniec túto metódu zavolá pre chýbajúce metódy, môžeme ju použiť na implementáciu niektorých trikov.
define_method
je metóda definovaná v Module
triedy, ktorú môžete použiť na dynamické vytváranie metód. Ak chcete použiť define_method
, zavoláte to s názvom novej metódy a blokom, kde sa parametre bloku stanú parametrami novej metódy. Aký je rozdiel medzi používaním def
vytvoriť metódu a define_method
? Nie je veľký rozdiel, ibaže môžete použiť define_method
v kombinácii s method_missing
na napísanie DRY kódu. Ak chcete byť presní, môžete použiť define_method
namiesto def
manipulovať s rozsahmi pri definovaní triedy, ale to je úplne iný príbeh. Pozrime sa na jednoduchý príklad:
class Developer define_method :frontend do |*my_arg| my_arg.inject(1, :*) end class < 100 p Developer.backend # undefined method 'backend' for Developer:Class (NoMethodError) Developer.create_backend p Developer.backend # 'Born from the ashes!'
To ukazuje, ako define_method
bol použitý na vytvorenie inštančnej metódy bez použitia def
. Môžeme s nimi však urobiť oveľa viac. Pozrime sa na tento útržok kódu:
class Developer def coding_frontend p 'writing frontend' end def coding_backend p 'writing backend' end end developer = Developer.new developer.coding_frontend # 'writing frontend' developer.coding_backend # 'writing backend'
Tento kód nie je SUCHÝ, ale používa sa define_method
môžeme to urobiť SUCHÉ:
class Developer ['frontend', 'backend'].each do |method| define_method 'coding_#{method}' do p 'writing ' + method.to_s end end end developer = Developer.new developer.coding_frontend # 'writing frontend' developer.coding_backend # 'writing backend'
To je oveľa lepšie, ale stále to nie je dokonalé. Prečo? Ak chceme pridať novú metódu coding_debug
napríklad musíme vložiť toto 'debug'
do poľa. Ale pomocou method_missing
môžeme to vyriešiť:
class Developer def method_missing method, *args, &block return super method, *args, &block unless method.to_s =~ /^coding_w+/ self.class.send(:define_method, method) do p 'writing ' + method.to_s.gsub(/^coding_/, '').to_s end self.send method, *args, &block end end developer = Developer.new developer.coding_frontend developer.coding_backend developer.coding_debug
Tento kód je trochu komplikovaný, takže si ho rozoberme. Zavolaním metódy, ktorá neexistuje, sa spustí method_missing
. Tu chceme vytvoriť novú metódu, iba ak názov metódy začína na 'coding_'
. Inak iba zavoláme super, aby sme nahlásili prácu o metóde, ktorá v skutočnosti chýba. A jednoducho používame define_method
vytvoriť túto novú metódu. To je všetko! S týmto kúskom kódu môžeme vytvoriť doslova tisíce nových metód počnúc 'coding_'
, a práve to robí náš kód SUCHÝ. Od define_method
stane sa súkromným pre Module
, musíme použiť send
vyvolať to.
Toto je iba vrchol ľadovca. Stať sa Ruby Jedi , toto je východiskový bod. Keď si osvojíte tieto stavebné prvky metaprogramovania a skutočne pochopíte jeho podstatu, môžete pokračovať v niečom zložitejšom, napríklad si vytvoriť svoj vlastný Jazyk špecifický pre doménu (DSL). DSL je téma sama o sebe, ale tieto základné pojmy sú nevyhnutným predpokladom na pochopenie pokročilých tém. Niektoré z najpoužívanejších drahokamov v Koľajnice boli postavené týmto spôsobom a pravdepodobne ste používali jeho DSL bez toho, aby ste o tom vedeli, napríklad RSpec a ActiveRecord.
Dúfajme, že vás tento článok priblíži o krok bližšie k pochopeniu metaprogramovania a možno aj k vytvoreniu vlastného protokolu DSL, ktorý môžete použiť na efektívnejšie kódovanie.