Metaprogramarea Ruby este chiar mai tare decât pare

Publicat: 2022-03-11

Auzi 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.

Ruby Metaprogramare: Cod de scriere a codului
Tweet

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:

Model 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:

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.