Ruby Metaprogrammering är ännu häftigare än det låter

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.

Ruby Metaprogrammering: Det bör noteras att metaprogrammering kan betyda mycket och det kan ofta missbrukas och gå till ytterligheter när det kommer till användning så jag kommer att försöka slänga in några exempel från verkligheten som alla kan använda sig av i den dagliga programmeringen.

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.

Lämna ett svar

Din e-postadress kommer inte publiceras.