Man hört oft, dass Metaprogrammierung etwas ist, das nur Ruby-Ninjas benutzen, und dass es einfach nichts für Normalsterbliche ist. Die Wahrheit ist jedoch, dass Metaprogrammierung überhaupt nichts Furchteinflößendes ist. Dieser Blog-Beitrag soll dazu dienen, diese Art des Denkens in Frage zu stellen und die Metaprogrammierung dem durchschnittlichen Ruby-Entwickler näher zu bringen, damit auch er von ihren Vorteilen profitieren kann.
Es sollte angemerkt werden, dass Metaprogrammierung viel bedeuten kann und dass sie oft sehr missbraucht werden kann und bis zum Äußersten gehen kann, wenn es um die Verwendung geht, daher werde ich versuchen, einige Beispiele aus der realen Welt einzubringen, die jeder in der alltäglichen Programmierung verwenden kann.
Metaprogrammierung
Metaprogrammierung ist eine Technik, mit der man Code schreiben kann, der zur Laufzeit dynamisch von selbst Code schreibt. Das heißt, man kann Methoden und Klassen während der Laufzeit definieren. Verrückt, oder? Kurz gesagt, mit Metaprogrammierung können Sie Klassen neu öffnen und ändern, Methoden abfangen, die nicht existieren, und sie während der Laufzeit erstellen, Code erstellen, der trocken ist, indem er Wiederholungen vermeidet, und vieles mehr.
Die Grundlagen
Bevor wir uns in die ernsthafte Metaprogrammierung stürzen, müssen wir die Grundlagen erkunden. Und das geht am besten anhand von Beispielen. Beginnen wir mit einem, um die Ruby-Metaprogrammierung Schritt für Schritt zu verstehen. Sie können wahrscheinlich erraten, was dieser Code macht:
class Developer def self.backend "I am backend developer" end def frontend "I am frontend developer" endend
Wir haben eine Klasse mit zwei Methoden definiert. Die erste Methode in dieser Klasse ist eine Klassenmethode und die zweite ist eine Instanzmethode. Das sind grundlegende Dinge in Ruby, aber hinter diesem Code steckt viel mehr, das wir verstehen müssen, bevor wir fortfahren. Es ist wichtig, darauf hinzuweisen, dass die Klasse Developer
selbst ein Objekt ist. In Ruby ist alles ein Objekt, auch Klassen. Da Developer
eine Instanz ist, ist es eine Instanz der Klasse 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>
Innerhalb von Klassenmethoden bezieht sich self
in gewisser Weise auf die Klasse selbst (worauf später in diesem Artikel noch näher eingegangen wird):
class Developer def self.backend self endendp Developer.backend# Developer
Das ist schön und gut, aber was ist denn nun eine Klassenmethode? Bevor wir diese Frage beantworten, müssen wir die Existenz von etwas erwähnen, das Metaklasse genannt wird, auch bekannt als Singleton-Klasse und Eigenklasse. Die Klassenmethode frontend
, die wir zuvor definiert haben, ist nichts anderes als eine Instanzmethode, die in der Metaklasse für das Objekt Developer
definiert ist! Eine Metaklasse ist im Wesentlichen eine Klasse, die Ruby erstellt und in die Vererbungshierarchie einfügt, um die Methoden der Klasse zu halten und so nicht mit den Instanzen zu interferieren, die von der Klasse erstellt werden.
Metaklassen
Jedes Objekt in Ruby hat seine eigene Metaklasse. Sie ist für einen Entwickler irgendwie unsichtbar, aber sie ist da und man kann sie sehr einfach benutzen. Da unsere Klasse Developer
im Wesentlichen ein Objekt ist, hat sie ihre eigene Metaklasse. Als Beispiel wollen wir ein Objekt der Klasse String
erstellen und seine Metaklasse manipulieren:
example = "I'm a string object"def example.something self.upcaseendp example.something# I'M A STRING OBJECT
Was wir hier getan haben, ist, dass wir eine Singleton-Methode something
zu einem Objekt hinzugefügt haben. Der Unterschied zwischen Klassenmethoden und Singleton-Methoden besteht darin, dass Klassenmethoden für alle Instanzen eines Klassenobjekts verfügbar sind, während Singleton-Methoden nur für diese eine Instanz verfügbar sind. Klassenmethoden sind weit verbreitet, während Singleton-Methoden nicht so häufig verwendet werden, aber beide Arten von Methoden werden einer Metaklasse des Objekts hinzugefügt.
Das vorherige Beispiel könnte wie folgt umgeschrieben werden:
example = "I'm a string object"class << example def something self.upcase endend
Die Syntax ist zwar anders, aber es wird effektiv das Gleiche getan. Kehren wir nun zum vorherigen Beispiel zurück, in dem wir die Klasse Developer
erstellt haben, und untersuchen wir einige andere Syntaxen, um eine Klassenmethode zu definieren:
class Developer def self.backend "I am backend developer" endend
Dies ist eine grundlegende Definition, die fast jeder verwendet.
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. Wir können dann die class << self
-Syntax verwenden, um das aktuelle Selbst so zu ändern, dass es auf die Metaklasse des aktuellen Objekts verweist. Da das aktuelle Objekt die Object
-Klasse selbst ist, wäre dies die Metaklasse der Instanz. Die Methode gibt self
zurück, das zu diesem Zeitpunkt selbst eine Metaklasse ist. Wenn wir also diese Instanzmethode für ein beliebiges Objekt aufrufen, können wir eine Metaklasse dieses Objekts erhalten. Definieren wir wieder unsere Developer
Klasse und beginnen wir ein wenig zu forschen:
Und zum krönenden Abschluss sehen wir uns den Beweis an, dass frontend
eine Instanzmethode einer Klasse ist und backend
eine Instanzmethode einer Metaklasse ist:
Um die Metaklasse zu erhalten, müssen Sie Object
nicht erneut öffnen und diesen Hack hinzufügen. Sie können singleton_class
verwenden, das Ruby zur Verfügung stellt. Es ist dasselbe wie das metaclass_example
, das wir hinzugefügt haben, aber mit diesem Hack kannst du tatsächlich sehen, wie Ruby unter der Haube arbeitet:
p developer.class.singleton_class.instance_methods false#
Methoden definieren mit „class_eval“ und „instance_eval“
Es gibt noch eine weitere Möglichkeit, eine Klassenmethode zu erstellen, und zwar mit instance_eval
:
Dieses Stück Code wertet der Ruby-Interpreter im Kontext einer Instanz aus, die in diesem Fall ein Developer
Objekt ist. Wenn Sie eine Methode für ein Objekt definieren, erstellen Sie entweder eine Klassenmethode oder eine Singleton-Methode. In diesem Fall ist es eine Klassenmethode – um genau zu sein, sind Klassenmethoden Singleton-Methoden, aber Singleton-Methoden einer Klasse, während die anderen Singleton-Methoden eines Objekts sind.
Auf der anderen Seite wertet class_eval
den Code im Kontext einer Klasse statt einer Instanz aus. Es öffnet praktisch die Klasse neu. Hier sehen Sie, wie class_eval
verwendet werden kann, um eine Instanzmethode zu erstellen:
Zusammenfassend lässt sich sagen, dass beim Aufruf der class_eval
-Methode self
auf die ursprüngliche Klasse verweist, und beim Aufruf von instance_eval
ändert sich self
und verweist auf die Metaklasse der ursprünglichen Klasse.
Fehlende Methoden spontan definieren
Ein weiteres Teil des Metaprogrammierungspuzzles ist method_missing
. Wenn man eine Methode für ein Objekt aufruft, geht Ruby zuerst in die Klasse und durchsucht ihre Instanzmethoden. Wenn er die Methode dort nicht findet, setzt er die Suche in der Kette der Vorfahren fort. Wenn Ruby die Methode immer noch nicht findet, ruft es eine andere Methode namens method_missing
auf, die eine Instanzmethode von Kernel
ist, die jedes Objekt erbt. Da wir sicher sind, dass Ruby diese Methode irgendwann für fehlende Methoden aufruft, können wir dies nutzen, um einige Tricks zu implementieren.
define_method
ist eine Methode, die in der Klasse Module
definiert ist und mit der man dynamisch Methoden erstellen kann. Um define_method
zu verwenden, rufen Sie es mit dem Namen der neuen Methode und einem Block auf, wobei die Parameter des Blocks die Parameter der neuen Methode werden. Was ist der Unterschied zwischen der Verwendung von def
zur Erstellung einer Methode und define_method
? Es gibt keinen großen Unterschied, außer dass Sie define_method
in Kombination mit method_missing
verwenden können, um DRY-Code zu schreiben. Um genau zu sein, können Sie define_method
anstelle von def
verwenden, um bei der Definition einer Klasse Bereiche zu manipulieren, aber das ist eine ganz andere Geschichte. Schauen wir uns ein einfaches Beispiel an:
Dies zeigt, wie define_method
verwendet wurde, um eine Instanzmethode zu erstellen, ohne ein def
zu verwenden. Es gibt jedoch noch viel mehr, was wir mit ihnen tun können. Werfen wir einen Blick auf diesen Codeschnipsel:
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"
Dieser Code ist nicht DRY, aber mit define_method
können wir ihn DRY machen:
Das ist viel besser, aber immer noch nicht perfekt. Warum eigentlich? Wenn wir zum Beispiel eine neue Methode coding_debug
hinzufügen wollen, müssen wir dieses "debug"
in das Array einfügen. Aber mit method_missing
können wir dies beheben:
Dieses Stück Code ist ein wenig kompliziert, also lassen Sie uns es aufschlüsseln. Der Aufruf einer Methode, die nicht existiert, löst ein method_missing
aus. Hier wollen wir eine neue Methode nur dann erstellen, wenn der Methodenname mit "coding_"
beginnt. Andernfalls rufen wir einfach super auf, um eine Methode zu melden, die eigentlich nicht vorhanden ist. Und wir verwenden einfach define_method
, um diese neue Methode zu erstellen. Das war’s! Mit diesem Stück Code können wir buchstäblich Tausende von neuen Methoden erstellen, die mit "coding_"
beginnen, und genau das macht unseren Code DRY. Da define_method
zufällig privat zu Module
ist, müssen wir send
verwenden, um es aufzurufen.
Zusammenfassung
Das ist nur die Spitze des Eisbergs. Um ein Ruby Jedi zu werden, ist dies der Ausgangspunkt. Wenn Sie diese Bausteine der Metaprogrammierung beherrschen und ihr Wesen wirklich verstehen, können Sie zu etwas Komplexerem übergehen, z. B. zur Erstellung einer eigenen domänenspezifischen Sprache (DSL). DSL ist ein Thema für sich, aber diese grundlegenden Konzepte sind eine Voraussetzung für das Verständnis fortgeschrittener Themen. Einige der meistverwendeten Gems in Rails wurden auf diese Weise entwickelt und Sie haben wahrscheinlich ihre DSL benutzt, ohne es zu wissen, wie z.B. RSpec und ActiveRecord.
Hoffentlich kann dieser Artikel Sie einen Schritt näher an das Verständnis der Metaprogrammierung heranbringen und vielleicht sogar Ihre eigene DSL erstellen, die Sie verwenden können, um effizienter zu programmieren.