Man hör ofta att metaprogrammering är något som bara Ruby ninjor använder, och att det helt enkelt inte är något för vanliga dödliga. Men sanningen är att metaprogrammering inte alls är något skrämmande. Det här blogginlägget kommer att tjäna till att utmana den här typen av tänkande och få metaprogrammering närmare den genomsnittlige Ruby-utvecklaren så att de också kan dra nytta av dess fördelar.
Metaprogrammering
Metaprogrammering är en teknik genom vilken du kan skriva kod som skriver kod av sig själv dynamiskt vid körning. Det innebär att du kan definiera metoder och klasser under körning. Galet, eller hur? I ett nötskal kan du med hjälp av metaprogrammering återöppna och ändra klasser, fånga metoder som inte finns och skapa dem i farten, skapa kod som är DRY genom att undvika upprepningar och mycket mer.
Grunderna
För att vi ska kunna dyka ner i seriös metaprogrammering måste vi utforska grunderna. Och det bästa sättet att göra det är genom exempel. Låt oss börja med ett och förstå Ruby-metaprogrammering steg för steg. Du kan förmodligen gissa vad den här koden gör:
class Developer def self.backend "I am backend developer" end def frontend "I am frontend developer" endend
Vi har definierat en klass med två metoder. Den första metoden i den här klassen är en klassmetod och den andra är en instansmetod. Detta är grundläggande saker i Ruby, men det händer mycket mer bakom denna kod som vi måste förstå innan vi går vidare. Det är värt att påpeka att klassen Developer
i sig själv faktiskt är ett objekt. I Ruby är allting ett objekt, inklusive klasser. Eftersom Developer
är en instans är den en instans av klassen 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>
Inför klassmetoder hänvisar self
till själva klassen på ett sätt (som kommer att diskuteras närmare senare i artikeln):
class Developer def self.backend self endendp Developer.backend# Developer
Detta är bra, men vad är en klassmetod egentligen? Innan vi svarar på den frågan måste vi nämna att det finns något som kallas metaklass, även känt som singletonklass och egenklass. Klassmetoden frontend
som vi definierade tidigare är inget annat än en instansmetod som definieras i metaklassen för objektet Developer
! En metaklass är i princip en klass som Ruby skapar och infogar i arvshierarkin för att hålla klassens metoder och därmed inte störa de instanser som skapas från klassen.
Metaklasser
Varje objekt i Ruby har sin egen metaklass. Den är på något sätt osynlig för en utvecklare, men den finns där och du kan använda den mycket enkelt. Eftersom vår klass Developer
i huvudsak är ett objekt har den sin egen metaklass. Som ett exempel kan vi skapa ett objekt av en klass String
och manipulera dess metaklass:
example = "I'm a string object"def example.something self.upcaseendp example.something# I'M A STRING OBJECT
Vad vi har gjort här är att vi har lagt till en singleton-metod something
till ett objekt. Skillnaden mellan klassmetoder och singletonmetoder är att klassmetoder är tillgängliga för alla instanser av ett klassobjekt medan singletonmetoder endast är tillgängliga för den enskilda instansen. Klassmetoder används i stor utsträckning medan singletonmetoder inte används lika mycket, men båda typerna av metoder läggs till i en metaklass för det objektet.
Det tidigare exemplet skulle kunna skrivas om så här:
example = "I'm a string object"class << example def something self.upcase endend
Syntaxen är annorlunda, men det gör i praktiken samma sak. Låt oss nu gå tillbaka till det tidigare exemplet där vi skapade Developer
klass och utforska några andra syntaxer för att definiera en klassmetod:
class Developer def self.backend "I am backend developer" endend
Detta är en grundläggande definition som nästan alla använder.
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. Vi kan sedan använda class << self
-syntaxen för att ändra det aktuella självet så att det pekar på metaklassen för det aktuella objektet. Eftersom det aktuella objektet är Object
klass självt skulle detta vara instansens metaklass. Metoden returnerar self
som vid denna tidpunkt själv är en metaklass. Så genom att anropa den här instansmetoden på ett objekt kan vi få en metaklass för det objektet. Låt oss definiera vår Developer
-klass igen och börja utforska lite:
Och för crescendot, låt oss se beviset för att frontend
är en instansmetod för en klass och backend
är en instansmetod för en metaklass:
För att få fram metaklassen behöver du dock inte öppna Object
på nytt och lägga till detta hack. Du kan använda singleton_class
som Ruby tillhandahåller. Det är samma sak som metaclass_example
som vi har lagt till, men med detta hack kan du faktiskt se hur Ruby fungerar under huven:
p developer.class.singleton_class.instance_methods false#
Definiera metoder med hjälp av ”class_eval” och ”instance_eval”
Det finns ytterligare ett sätt att skapa en klassmetod, och det är genom att använda instance_eval
:
Detta kodstycke utvärderar Ruby-tolkaren i samband med en instans, vilket i det här fallet är ett Developer
-objekt. När du definierar en metod på ett objekt skapar du antingen en klassmetod eller en singletonmetod. I det här fallet är det en klassmetod – för att vara exakt är klassmetoder singletonmetoder men singletonmetoder för en klass, medan de andra är singletonmetoder för ett objekt.
Å andra sidan utvärderar class_eval
koden i kontexten för en klass istället för en instans. Den öppnar praktiskt taget klassen på nytt. Här är hur class_eval
kan användas för att skapa en instansmetod:
För att sammanfatta: När du anropar class_eval
-metoden ändrar du self
så att den hänvisar till den ursprungliga klassen och när du anropar instance_eval
ändrar så att den hänvisar till den ursprungliga klassens metaklass.
Definiera saknade metoder i farten
En ytterligare pusselbit i metaprogrammeringspusslet är method_missing
. När du anropar en metod på ett objekt går Ruby först in i klassen och söker igenom dess instansmetoder. Om den inte hittar metoden där fortsätter den att söka uppåt i anhörigkedjan. Om Ruby fortfarande inte hittar metoden anropar den en annan metod som heter method_missing
som är en instansmetod för Kernel
som alla objekt ärver. Eftersom vi är säkra på att Ruby kommer att anropa den här metoden så småningom för saknade metoder kan vi använda detta för att implementera några knep.
define_method
är en metod definierad i Module
klassen som du kan använda för att skapa metoder dynamiskt. För att använda define_method
anropar du den med namnet på den nya metoden och ett block där blockets parametrar blir parametrar för den nya metoden. Vad är skillnaden mellan att använda def
för att skapa en metod och define_method
? Det är ingen större skillnad förutom att du kan använda define_method
i kombination med method_missing
för att skriva DRY-kod. För att vara exakt kan du använda define_method
i stället för def
för att manipulera scopes när du definierar en klass, men det är en helt annan historia. Låt oss ta en titt på ett enkelt exempel:
Detta visar hur define_method
användes för att skapa en instansmetod utan att använda en def
. Det finns dock mycket mer vi kan göra med dem. Låt oss ta en titt på detta kodutdrag:
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"
Denna kod är inte DRY, men med hjälp av define_method
kan vi göra den DRY:
Det är mycket bättre, men fortfarande inte perfekt. Varför? Om vi till exempel vill lägga till en ny metod coding_debug
måste vi lägga in denna "debug"
i arrayen. Men med hjälp av method_missing
kan vi åtgärda detta:
Den här delen av koden är lite komplicerad så låt oss dela upp den. Att anropa en metod som inte finns kommer att starta method_missing
. Här vill vi skapa en ny metod endast när metodnamnet börjar med "coding_"
. Annars anropar vi bara super för att göra arbetet med att rapportera en metod som faktiskt saknas. Och vi använder helt enkelt define_method
för att skapa den nya metoden. Så är det! Med den här delen av koden kan vi skapa bokstavligen tusentals nya metoder som börjar med "coding_"
, och det är detta faktum som gör vår kod DRY. Eftersom define_method
råkar vara privat för Module
måste vi använda send
för att åberopa den.
Avsluta
Det här är bara toppen av isberget. För att bli en Ruby Jedi är det här utgångspunkten. När du behärskar dessa byggstenar för metaprogrammering och verkligen förstår dess essens kan du gå vidare till något mer komplext, till exempel skapa ditt eget domänspecifika språk (DSL). DSL är ett ämne i sig, men dessa grundläggande begrepp är en förutsättning för att förstå avancerade ämnen. Några av de mest använda gems i Rails byggdes på det här sättet och du har förmodligen använt dess DSL utan att veta om det, till exempel RSpec och ActiveRecord.
Förhoppningsvis kan den här artikeln ta dig ett steg närmare förståelsen av metaprogrammering och kanske till och med bygga ett eget DSL, som du kan använda för att koda mer effektivt.