De multe ori auziți că metaprogramarea este ceva ce folosesc doar ninja Ruby și că pur și simplu nu este pentru muritorii de rând. Dar adevărul este că metaprogramarea nu este ceva deloc înfricoșător. Această postare pe blog va avea rolul de a contesta acest tip de gândire și de a aduce metaprogramarea mai aproape de dezvoltatorul Ruby obișnuit, astfel încât și acesta să poată profita de beneficiile sale.
Ar trebui remarcat faptul că metaprogramarea ar putea însemna foarte mult și poate fi adesea foarte prost folosită și poate merge la extrem atunci când vine vorba de utilizare, așa că voi încerca să arunc 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 singur cod în mod dinamic în timpul execuției. Acest lucru înseamnă că puteți defini metode și clase în timpul execuției. O nebunie, nu-i așa? Pe scurt, folosind metaprogramarea puteți redeschide și modifica clasele, puteți prinde metode care nu există și le puteți crea din mers, puteți crea cod care este DRY prin evitarea repetițiilor și multe altele.
Bazele
Până să ne scufundăm în metaprogramarea serioasă trebuie să explorăm elementele de bază. Iar cel mai bun mod de a face acest lucru este prin exemple. Haideți să începem cu unul și să înțelegem metaprogramarea Ruby pas cu pas. Probabil că puteți ghici ce face acest cod:
class Developer def self.backend "I am backend developer" end def frontend "I am frontend developer" endend
Am definit o clasă cu două metode. Prima metodă din această clasă este o metodă de clasă, iar cea de-a doua este o metodă de instanță. Acestea sunt lucruri de bază în Ruby, dar în spatele acestui cod se întâmplă mult mai multe lucruri pe care trebuie să le înțelegem înainte de a merge mai departe. Merită să subliniem faptul că clasa Developer
însăși este de fapt un obiect. În Ruby, totul este un obiect, inclusiv clasele. Din moment ce Developer
este o instanță, ea este o instanță a clasei Class
. Here is how the Ruby object model looks like:
p Developer.class # Classp Class.superclass # Modulep Module.superclass # Objectp Object.superclass # BasicObject
One important thing to understand here is the meaning of self
. The frontend
method is a regular method that is available on instances of class Developer
, but why is backend
method a class method? Every piece of code executed in Ruby is executed against a particular self. When the Ruby interpreter executes any code it always keeps track of the value self
for any given line. self
is always referring to some object but that object can change based on the code executed. For example, inside a class definition, the self
refers to the class itself which is an instance of class Class
.
class Developer p self end# Developer
Inside instance methods, self
refers to an instance of the class.
class Developer def frontend self endend p Developer.new.frontend# #<Developer:0x2c8a148>
În interiorul metodelor de clasă, self
se referă la clasa însăși într-un mod (care va fi discutat mai detaliat mai târziu în acest articol):
class Developer def self.backend self endendp Developer.backend# Developer
Este bine, dar ce este, până la urmă, o metodă de clasă? Înainte de a răspunde la această întrebare, trebuie să menționăm existența a ceva numit metaclasă, cunoscută și sub numele de clasă singleton și clasă proprie. Metoda de clasă frontend
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, astfel încât să nu interfereze cu instanțele care sunt create din clasă.
Metaclase
Care obiect din Ruby are propria metaclasă. Aceasta este cumva invizibilă pentru un dezvoltator, dar este acolo și o puteți folosi foarte ușor. Deoarece clasa noastră Developer
este în esență un obiect, aceasta are propria metaclasă. Ca exemplu, haideți să creăm un obiect din clasa String
și să-i manipulăm metaclasa:
example = "I'm a string object"def example.something self.upcaseendp example.something# I'M A STRING OBJECT
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 doar pentru acea singură instanță. Metodele de clasă sunt utilizate pe scară largă, în timp ce metodele singleton nu atât de mult, dar 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 endend
Sintaxa este diferită, dar face efectiv același lucru. Acum să ne întoarcem la exemplul anterior în care am creat clasa Developer
și să explorăm alte sintaxe pentru a defini o metodă de clasă:
class Developer def self.backend "I am backend developer" endend
Aceasta este o definiție de bază pe care o folosește aproape toată lumea.
def Developer.backend "I am backend developer"end
This is the same thing, we are defining the backend
class method for Developer
. We didn’t use self
but defining a method like this effectively makes it a class method.
class Developer class << self def backend "I am backend developer" end endend
Again, we are defining a class method, but using syntax similar to one we used to define a singleton method for a String
object. You may notice that we used self
here which refers to a Developer
object itself. First we opened Developer
class, making self equal to the Developer
class. Next, we do class << self
, making self equal to Developer
‘s metaclass. Then we define a method backend
on Developer
‘s metaclass.
class << Developer def backend "I am backend developer" endend
By defining a block like this, we are setting self
to Developer
‘s metaclass for the duration of the block. As a result, the backend
method is added to Developer
‘s metaclass, rather than the class itself.
Let’s see how this metaclass behaves in the inheritance tree:
As you saw in previous examples, there’s no real proof that metaclass even exists. But we can use a little hack that can show us the existence of this invisible class:
class Object def metaclass_example class << self self end endend
If we define an instance method in Object
class (yes, we can reopen any class anytime, that’s yet another beauty of metaprogramming), we will have a self
referring to the Object
object inside it. Putem folosi apoi sintaxa class << self
pentru a schimba self-ul curent pentru a indica metaclasa obiectului curent. Deoarece obiectul curent este chiar clasa Object
, aceasta ar fi metaclasa instanței. Metoda returnează self
care în acest moment este ea însăși o metaclasă. Așadar, prin apelarea acestei metode de instanță pe orice obiect putem obține o metaclasă a acelui obiect. Să definim din nou clasa noastră Developer
și să începem să explorăm puțin:
Ș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:
Deși, pentru a obține metaclasa nu este nevoie să redeschideți efectiv Object
și să adăugați acest hack. Puteți folosi singleton_class
pe care Ruby îl pune la dispoziție. Este la fel ca 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#
Definirea metodelor folosind „class_eval” și „instance_eval”
Există încă o modalitate de a crea o metodă de clasă, și aceasta este folosind instance_eval
:
Această bucată de cod interpretul Ruby o evaluează în contextul unei instanțe, care este în acest caz un obiect Developer
. Iar atunci 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ă – mai 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 și nu al unei instanțe. Practic, redeschide clasa. Iată cum poate fi folosit class_eval
pentru a crea o metodă de instanță:
Pentru a rezuma, când apelați metoda class_eval
, schimbați self
pentru a se referi la clasa originală, iar când apelați instance_eval
self
se schimbă pentru a se referi la metaclasa clasei originale.
Definirea din mers a metodelor lipsă
Încă o piesă din puzzle-ul metaprogramării este method_missing
. Când apelați o metodă pe un obiect, Ruby intră mai întâi în clasă și parcurge metodele sale de instanță. Dacă nu găsește metoda acolo, continuă căutarea în susul lanțului de strămoși. Dacă Ruby tot nu găsește metoda, apelează o altă metodă numită method_missing
, care este o metodă de instanță a Kernel
pe care o moștenește fiecare obiect. Din moment ce suntem siguri că Ruby va apela această metodă în cele din urmă pentru metodele lipsă, putem folosi acest lucru pentru a implementa unele trucuri.
define_method
este o metodă definită în clasa Module
pe care o puteți folosi pentru a crea metode în mod dinamic. Pentru a utiliza define_method
, o apelați cu numele noii metode și un bloc în care parametrii blocului devin parametrii noii metode. Care este diferența dintre utilizarea def
pentru a crea o metodă și define_method
? Nu există prea multe diferențe, cu excepția faptului că puteți utiliza define_method
în combinație cu method_missing
pentru a scrie cod DRY. Mai exact, puteți folosi define_method
în loc de def
pentru a manipula domeniile de cuprindere atunci când definiți o clasă, dar aceasta este o cu totul altă poveste. Să ne uităm la un exemplu simplu:
Aceasta arată cum define_method
a fost folosit pentru a crea o metodă de instanță fără a folosi un def
. Cu toate acestea, putem face mult mai multe cu ele. Să ne uităm la acest fragment de cod:
class Developer def coding_frontend p "writing frontend" end def coding_backend p "writing backend" endenddeveloper = Developer.newdeveloper.coding_frontend# "writing frontend"developer.coding_backend# "writing backend"
Acest cod nu este DRY, dar folosind define_method
îl putem face DRY:
Este mult mai bine, dar tot nu este 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 rezolva acest lucru:
Această bucată de cod este puțin complicată, așa că haideți să o despărțim. Apelarea unei metode care nu există va declanșa method_missing
. Aici, dorim să creăm o metodă nouă doar atunci când numele metodei începe cu "coding_"
. În caz contrar, apelăm pur și simplu super pentru a face munca de raportare a unei metode care lipsește de fapt. Iar noi folosim pur și simplu define_method
pentru a crea acea metodă nouă. Asta este! Cu această bucată de cod putem crea literalmente mii de metode noi începând cu "coding_"
, iar acest fapt este ceea ce face ca codul nostru să fie DRY. Deoarece define_method
se întâmplă să fie privată pentru Module
, trebuie să folosim send
pentru a o invoca.
Încheiere
Acesta este doar vârful icebergului. Pentru a deveni un Jedi Ruby, acesta este punctul de plecare. După ce stăpâniți aceste elemente constitutive ale metaprogramării și îi înțelegeți cu adevărat esența, 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 a înțelege subiectele avansate. Unele dintre cele mai utilizate pietre prețioase din Rails au fost construite în acest mod ș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 metaprogramării și poate chiar de construirea propriului DSL, pe care îl puteți folosi pentru a codifica mai eficient.
.