Metaprogramarea Ruby este chiar mai tare decât pare
Publicat: 2022-03-11Auzi adesea că metaprogramarea este ceva pe care doar ninja-urile Ruby îl folosesc și că pur și simplu nu este pentru muritorii obișnuiți. Dar adevărul este că metaprogramarea nu este deloc ceva înfricoșător. Această postare pe blog va servi pentru a provoca acest tip de gândire și pentru a apropia metaprogramarea de dezvoltatorul Ruby obișnuit, astfel încât aceștia să poată culege și beneficiile acesteia.
Trebuie remarcat faptul că metaprogramarea ar putea însemna mult și poate fi adesea foarte greșit și poate ajunge la extrem atunci când vine vorba de utilizare, așa că voi încerca să introduc câteva exemple din lumea reală pe care toată lumea le-ar putea folosi în programarea de zi cu zi.
Metaprogramarea
Metaprogramarea este o tehnică prin care puteți scrie cod care scrie cod de la sine în mod dinamic în timpul execuției. Aceasta înseamnă că puteți defini metode și clase în timpul rulării. Nebun, nu? Pe scurt, folosind metaprogramarea puteți redeschide și modifica clase, puteți prinde metode care nu există și le puteți crea din mers, puteți crea cod DRY evitând repetări și multe altele.
Cele elementare
Înainte de a ne scufunda în metaprogramarea serioasă, trebuie să explorăm elementele de bază. Și cel mai bun mod de a face asta este prin exemplu. Să începem cu unul și să înțelegem pas cu pas metaprogramarea Ruby. Probabil puteți ghici ce face acest cod:
class Developer def self.backend "I am backend developer" end def frontend "I am frontend developer" end end
Am definit o clasă cu două metode. Prima metodă din această clasă este o metodă de clasă, iar a doua este o metodă de instanță. Acestea sunt lucruri de bază în Ruby, dar se întâmplă mult mai multe în spatele acestui cod, pe care trebuie să le înțelegem înainte de a continua. Merită subliniat că clasa Developer
în sine este de fapt un obiect. În Ruby totul este un obiect, inclusiv clasele. Deoarece Developer
este o instanță, este o instanță a clasei Class
. Iată cum arată modelul obiect Ruby:
p Developer.class # Class p Class.superclass # Module p Module.superclass # Object p Object.superclass # BasicObject
Un lucru important de înțeles aici este semnificația self
. Metoda frontend
este o metodă obișnuită care este disponibilă pe instanțe ale clasei Developer
, dar de ce este metoda backend
o metodă de clasă? Fiecare bucată de cod executată în Ruby este executată împotriva unui anumit sine . Când interpretul Ruby execută orice cod, ține întotdeauna evidența valorii self
pentru orice linie dată. self
se referă întotdeauna la un obiect, dar acel obiect se poate schimba în funcție de codul executat. De exemplu, în interiorul unei definiții de clasă, self
se referă la clasa în sine, care este o instanță a clasei Class
.
class Developer p self end # Developer
În cadrul metodelor de instanță, self
se referă la o instanță a clasei.
class Developer def frontend self end end p Developer.new.frontend # #<Developer:0x2c8a148>
În cadrul metodelor de clasă, self
se referă la clasa în sine într-un fel (care va fi discutat mai detaliat mai târziu în acest articol):
class Developer def self.backend self end end p Developer.backend # Developer
Este bine, dar ce este o metodă de clasă până la urmă? Înainte de a răspunde la această întrebare trebuie să menționăm existența a ceva numit metaclasă, cunoscută și sub denumirea de clasă singleton și eigenclass. frontend
metodei clasei pe care am definit-o mai devreme nu este altceva decât o metodă de instanță definită în metaclasa pentru obiectul Developer
! O metaclasă este în esență o clasă pe care Ruby o creează și o inserează în ierarhia de moștenire pentru a păstra metodele clasei, fără a interfera astfel cu instanțe care sunt create din clasă.
Metaclase
Fiecare obiect din Ruby are propria sa metaclasă. Este cumva invizibil pentru un dezvoltator, dar este acolo și îl poți folosi foarte ușor. Deoarece clasa noastră Developer
este în esență un obiect, are propria sa metaclasă. Ca exemplu, să creăm un obiect al unei clase String
și să manipulăm metaclasa acestuia:
example = "I'm a string object" def example.something self.upcase end p example.something # I'M A STRING OBJECT
Ceea ce am făcut aici este că am adăugat o metodă singleton something
la un obiect. Diferența dintre metodele de clasă și metodele singleton este că metodele de clasă sunt disponibile pentru toate instanțele unui obiect de clasă, în timp ce metodele singleton sunt disponibile numai pentru acea instanță unică. Metodele de clasă sunt utilizate pe scară largă, în timp ce metodele singleton nu atât de mult, ci ambele tipuri de metode sunt adăugate la o metaclasă a acelui obiect.
Exemplul anterior ar putea fi rescris astfel:
example = "I'm a string object" class << example def something self.upcase end end
Sintaxa este diferită, dar realizează același lucru. Acum să ne întoarcem la exemplul anterior în care am creat clasa Developer
și să explorăm câteva alte sintaxe pentru a defini o metodă de clasă:
class Developer def self.backend "I am backend developer" end end
Aceasta este o definiție de bază pe care o folosește aproape toată lumea.
def Developer.backend "I am backend developer" end
Acesta este același lucru, definim metoda clasei backend
pentru Developer
. Nu am folosit self
, dar definirea unei metode ca aceasta o face efectiv o metodă de clasă.
class Developer class << self def backend "I am backend developer" end end end
Din nou, definim o metodă de clasă, dar utilizând o sintaxă similară cu cea pe care am folosit-o pentru a defini o metodă singleton pentru un obiect String
. Puteți observa că am folosit self
aici, care se referă la un obiect Developer
în sine. Mai întâi am deschis clasa Developer
, făcându-l egal cu clasa Developer
. Apoi, facem class << self
, făcând self egal cu metaclasa Developer
. Apoi definim un backend
de metodă pe metaclasa Developer
.
class << Developer def backend "I am backend developer" end end
Prin definirea unui bloc ca acesta, ne setăm pe self
la metaclasa Developer
pe durata blocului. Ca rezultat, metoda backend
este adăugată la metaclasa Developer
, mai degrabă decât la clasa în sine.
Să vedem cum se comportă această metaclasă în arborele de moștenire:
După cum ați văzut în exemplele anterioare, nu există nicio dovadă reală că metaclasa chiar există. Dar putem folosi un mic hack care ne poate arăta existența acestei clase invizibile:
class Object def metaclass_example class << self self end end end
Dacă definim o metodă de instanță în clasa Object
(da, putem redeschide orice clasă oricând, aceasta este încă o frumusețe a metaprogramarii), vom avea o self
-referire la obiectul Object
din interiorul acesteia. Apoi putem folosi sintaxa class << self
pentru a schimba eul curent pentru a indica metaclasa obiectului curent. Deoarece obiectul curent este însăși clasa Object
, aceasta ar fi metaclasa instanței. Metoda returnează self
, care este în acest moment o metaclasă în sine. Deci, apelând această metodă de instanță pe orice obiect putem obține o metaclasă a acelui obiect. Să definim din nou clasa noastră de Developer
și să începem să explorăm puțin:

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:0x2ced3b8>" Developer.backend # "inside class method, self is: Developer" p "inside metaclass, self is: " + developer.metaclass_example.to_s # "inside metaclass, self is: #<Class:#<Developer:0x2ced3b8>>"
Și pentru crescendo, să vedem dovada că frontend
este o metodă de instanță a unei clase și backend
este o metodă de instanță a unei metaclase:
p developer.class.instance_methods false # [:frontend] p developer.class.metaclass_example.instance_methods false # [:backend]
Deși, pentru a obține metaclasa, nu trebuie să redeschideți de fapt Object
și să adăugați acest hack. Puteți folosi singleton_class
pe care o oferă Ruby. Este același cu metaclass_example
pe care l-am adăugat, dar cu acest hack puteți vedea de fapt cum funcționează Ruby sub capotă:
p developer.class.singleton_class.instance_methods false # [:backend]
Definirea metodelor folosind „class_eval” și „instance_eval”
Mai există o modalitate de a crea o metodă de clasă, și anume prin utilizarea 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"
Această bucată de cod Ruby interpret evaluează în contextul unei instanțe, care este în acest caz un obiect Developer
. Și când definiți o metodă pe un obiect, creați fie o metodă de clasă, fie o metodă singleton. În acest caz, este o metodă de clasă - pentru a fi exact, metodele de clasă sunt metode singleton, dar metode singleton ale unei clase, în timp ce celelalte sunt metode singleton ale unui obiect.
Pe de altă parte, class_eval
evaluează codul în contextul unei clase în loc de o instanță. Practic redeschide clasa. Iată cum poate fi folosit class_eval
pentru a crea o metodă de instanță:
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:0x2c5d640> developer.frontend # "inside a method self is: #<Developer:0x2c5d640>"
Pentru a rezuma, atunci când apelați metoda class_eval
, schimbați self
pentru a face referire la clasa originală și când apelați instance_eval
, self
schimbă pentru a se referi la metaclasa clasei originale.
Definirea metodelor lipsă în zbor
Încă o piesă a puzzle-ului de metaprogramare este method_missing
. Când apelați o metodă pe un obiect, Ruby intră mai întâi în clasă și răsfoiește metodele de instanță ale acesteia. Dacă nu găsește metoda acolo, continuă căutarea în lanțul strămoșilor. Dacă Ruby încă nu găsește metoda, apelează o altă metodă numită method_missing
, care este o metodă de instanță a Kernel
-ului pe care fiecare obiect o moștenește. Deoarece suntem siguri că Ruby va apela această metodă în cele din urmă pentru metodele lipsă, putem folosi aceasta pentru a implementa câteva trucuri.
define_method
este o metodă definită în clasa Module
pe care o puteți folosi pentru a crea metode dinamic. Pentru a utiliza define_method
, îl apelați cu numele noii metode și un bloc în care parametrii blocului devin parametrii noii metode. Care este diferența dintre folosirea def
pentru a crea o metodă și define_method
? Nu există mare diferență, cu excepția faptului că puteți utiliza define_method
în combinație cu method_missing
pentru a scrie codul DRY. Pentru a fi exact, puteți folosi define_method
în loc de def
pentru a manipula domeniile atunci când definiți o clasă, dar asta este cu totul altă poveste. Să aruncăm o privire la un exemplu simplu:
class Developer define_method :frontend do |*my_arg| my_arg.inject(1, :*) end class << self def create_backend singleton_class.send(:define_method, "backend") do "Born from the ashes!" end end end end developer = Developer.new p developer.frontend(2, 5, 10) # => 100 p Developer.backend # undefined method 'backend' for Developer:Class (NoMethodError) Developer.create_backend p Developer.backend # "Born from the ashes!"
Aceasta arată cum define_method
a fost folosit pentru a crea o metodă de instanță fără a utiliza un def
. Cu toate acestea, putem face mult mai multe cu ele. Să aruncăm o privire la acest fragment de cod:
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"
Acest cod nu este DRY, dar folosind define_method
îl putem face DRY:
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"
E mult mai bine, dar tot nu perfect. De ce? Dacă vrem să adăugăm o nouă metodă coding_debug
, de exemplu, trebuie să punem acest "debug"
în matrice. Dar folosind method_missing
putem remedia acest lucru:
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
Această bucată de cod este puțin complicată, așa că haideți să o descompunem. Apelarea unei metode care nu există va declanșa method_missing
. Aici, dorim să creăm o nouă metodă numai atunci când numele metodei începe cu "coding_"
. În caz contrar, numim decât super pentru a face munca de raportare a unei metode care lipsește de fapt. Și pur și simplu folosim define_method
pentru a crea acea nouă metodă. Asta e! Cu această bucată de cod putem crea literalmente mii de metode noi, începând cu "coding_"
, iar acest fapt este ceea ce face codul nostru USCAT. Deoarece define_method
se întâmplă să fie privat pentru Module
, trebuie să folosim send
pentru a-l invoca.
Încheierea
Acesta este doar vârful aisbergului. Pentru a deveni un Ruby Jedi, acesta este punctul de plecare. După ce stăpâniți aceste blocuri de construcție ale metaprogramarii și înțelegeți cu adevărat esența acesteia, puteți trece la ceva mai complex, de exemplu să vă creați propriul limbaj specific domeniului (DSL). DSL este un subiect în sine, dar aceste concepte de bază sunt o condiție prealabilă pentru înțelegerea subiectelor avansate. Unele dintre cele mai utilizate pietre prețioase din Rails au fost construite în acest fel și probabil că ați folosit DSL-ul său fără să știți, cum ar fi RSpec și ActiveRecord.
Sperăm că acest articol vă poate aduce cu un pas mai aproape de înțelegerea metaprogramarii și poate chiar de a vă construi propriul DSL, pe care îl puteți folosi pentru a codifica mai eficient.