Ruby-Metaprogrammierung ist noch cooler, als es sich anhört
Veröffentlicht: 2022-03-11Sie hören oft, dass Metaprogrammierung etwas ist, das nur Ruby-Ninjas verwenden, und dass es einfach nichts für gewöhnliche Sterbliche ist. Aber die Wahrheit ist, dass Metaprogrammierung überhaupt nichts Beängstigendes ist. Dieser Blogbeitrag soll dazu dienen, diese Art des Denkens in Frage zu stellen und dem durchschnittlichen Ruby-Entwickler die Metaprogrammierung näher zu bringen, damit auch sie von den Vorteilen profitieren können.
Es sollte beachtet werden, dass Metaprogrammierung viel bedeuten kann und oft sehr missbraucht werden kann, wenn es um die Verwendung geht, also werde ich versuchen, einige Beispiele aus der realen Welt einzufügen, die jeder in der täglichen Programmierung verwenden könnte.
Metaprogrammierung
Metaprogrammierung ist eine Technik, mit der Sie Code schreiben können, der zur Laufzeit selbst dynamisch Code schreibt . Das bedeutet, dass Sie zur Laufzeit Methoden und Klassen definieren können. Verrückt, oder? Kurz gesagt, mit Metaprogrammierung können Sie Klassen neu öffnen und ändern, Methoden, die nicht existieren, abfangen und spontan erstellen, Code erstellen, der DRY ist, indem Wiederholungen vermieden werden, und vieles mehr.
Die Grundlagen
Bevor wir in die ernsthafte Metaprogrammierung eintauchen, müssen wir die Grundlagen erforschen. Und das geht am besten mit gutem Beispiel voran. Beginnen wir mit einem und verstehen die Ruby-Metaprogrammierung Schritt für Schritt. Sie können wahrscheinlich erraten, was dieser Code tut:
class Developer def self.backend "I am backend developer" end def frontend "I am frontend developer" end end
Wir haben eine Klasse mit zwei Methoden definiert. Die erste Methode in dieser Klasse ist eine Klassenmethode und die zweite eine Instanzmethode. Dies sind grundlegende Dinge in Ruby, aber hinter diesem Code passiert noch viel mehr, was wir verstehen müssen, bevor wir fortfahren. Es sei darauf hingewiesen, dass die Klasse Developer
selbst eigentlich ein Objekt ist. In Ruby ist alles ein Objekt, einschließlich Klassen. Da Developer
eine Instanz ist, ist es eine Instanz der Klasse Class
. So sieht das Ruby-Objektmodell aus:
p Developer.class # Class p Class.superclass # Module p Module.superclass # Object p Object.superclass # BasicObject
Eine wichtige Sache, die es hier zu verstehen gilt, ist die Bedeutung des self
. Die frontend
-Methode ist eine reguläre Methode, die auf Instanzen der Klasse Developer
verfügbar ist, aber warum ist die backend
-Methode eine Klassenmethode? Jeder Codeabschnitt, der in Ruby ausgeführt wird, wird gegen ein bestimmtes Selbst ausgeführt. Wenn der Ruby-Interpreter irgendeinen Code ausführt, verfolgt er immer den Wert self
für jede gegebene Zeile. self
bezieht sich immer auf ein Objekt, aber dieses Objekt kann sich basierend auf dem ausgeführten Code ändern. Beispielsweise bezieht sich self
innerhalb einer Klassendefinition auf die Klasse selbst, die eine Instanz der Klasse Class
ist.
class Developer p self end # Developer
Innerhalb von Instanzmethoden bezieht sich self
auf eine Instanz der Klasse.
class Developer def frontend self end end p Developer.new.frontend # #<Developer:0x2c8a148>
Innerhalb von Klassenmethoden bezieht sich self
in gewisser Weise auf die Klasse selbst (was später in diesem Artikel ausführlicher besprochen wird):
class Developer def self.backend self end end p Developer.backend # Developer
Das ist in Ordnung, aber was ist überhaupt eine Klassenmethode? Bevor wir diese Frage beantworten, müssen wir die Existenz einer so genannten Metaklasse erwähnen, die auch als Singleton-Klasse und Eigenklasse bekannt ist. Das Klassenmethoden- frontend
, das 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 Klassenmethoden zu enthalten, wodurch Instanzen, die von der Klasse erstellt werden, nicht beeinträchtigt werden.
Metaklassen
Jedes Objekt in Ruby hat seine eigene Metaklasse. Es ist für einen Entwickler irgendwie unsichtbar, aber es ist da und Sie können es sehr einfach verwenden. Da unsere Klasse Developer
im Wesentlichen ein Objekt ist, hat sie ihre eigene Metaklasse. Lassen Sie uns als Beispiel ein Objekt der Klasse String
erstellen und seine Metaklasse manipulieren:
example = "I'm a string object" def example.something self.upcase end p example.something # I'M A STRING OBJECT
Was wir hier getan haben, ist, dass wir einem Objekt eine Singleton-Methode something
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 einzelne Instanz verfügbar sind. Klassenmethoden werden häufig verwendet, während Singleton-Methoden nicht so sehr verwendet werden, aber beide Arten von Methoden werden einer Metaklasse dieses Objekts hinzugefügt.
Das vorherige Beispiel könnte wie folgt umgeschrieben werden:
example = "I'm a string object" class << example def something self.upcase end end
Die Syntax ist anders, aber sie macht effektiv dasselbe. Lassen Sie uns nun zum vorherigen Beispiel zurückkehren, in dem wir die Developer
-Klasse erstellt haben, und einige andere Syntaxen untersuchen, um eine Klassenmethode zu definieren:
class Developer def self.backend "I am backend developer" end end
Dies ist eine grundlegende Definition, die fast jeder verwendet.
def Developer.backend "I am backend developer" end
Das ist dasselbe, wir definieren die backend
-Klassenmethode für Developer
. Wir haben self
nicht verwendet, aber die Definition einer Methode wie dieser macht sie effektiv zu einer Klassenmethode.
class Developer class << self def backend "I am backend developer" end end end
Auch hier definieren wir eine Klassenmethode, verwenden jedoch eine ähnliche Syntax wie bei der Definition einer Singleton-Methode für ein String
-Objekt. Sie werden vielleicht bemerken, dass wir hier self
verwendet haben, was sich auf ein Developer
Objekt selbst bezieht. Zuerst haben wir die Developer
-Klasse geöffnet und uns selbst der Developer
-Klasse gleichgestellt. Als nächstes führen wir class << self
aus, wodurch self gleich der Metaklasse von Developer
wird. Dann definieren wir ein Methoden- backend
für die Metaklasse von Developer
.
class << Developer def backend "I am backend developer" end end
Indem wir einen solchen Block definieren, setzen wir self
für die Dauer des Blocks auf die Metaklasse von Developer
. Als Ergebnis wird die backend
-Methode zur Metaklasse von Developer
hinzugefügt und nicht zur Klasse selbst.
Mal sehen, wie sich diese Metaklasse im Vererbungsbaum verhält:
Wie Sie in den vorherigen Beispielen gesehen haben, gibt es keinen wirklichen Beweis dafür, dass Metaklassen überhaupt existieren. Aber wir können einen kleinen Hack verwenden, der uns die Existenz dieser unsichtbaren Klasse zeigt:
class Object def metaclass_example class << self self end end end
Wenn wir eine Instanzmethode in der Object
-Klasse definieren (ja, wir können jede Klasse jederzeit wieder öffnen, das ist eine weitere Schönheit der Metaprogrammierung), haben wir ein self
, das auf das darin Object
Objekt verweist. Wir können dann die Syntax class << self
verwenden, um das aktuelle self so zu ändern, dass es auf die Metaklasse des aktuellen Objekts zeigt. Da das aktuelle Object
die Objektklasse selbst ist, wäre dies die Metaklasse der Instanz. Die Methode gibt self
zurück, was zu diesem Zeitpunkt selbst eine Metaklasse ist. Durch Aufrufen dieser Instanzmethode für ein beliebiges Objekt können wir also eine Metaklasse dieses Objekts erhalten. Lassen Sie uns unsere Developer
-Klasse erneut definieren und ein wenig erkunden:

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>>"
Und für das Crescendo sehen wir uns den Beweis an, dass frontend
eine Instanzmethode einer Klasse und backend
eine Instanzmethode einer Metaklasse ist:
p developer.class.instance_methods false # [:frontend] p developer.class.metaclass_example.instance_methods false # [:backend]
Um die Metaklasse zu erhalten, müssen Sie Object
jedoch nicht erneut öffnen und diesen Hack hinzufügen. Sie können die von Ruby singleton_class
verwenden. Es ist dasselbe wie metaclass_example
, das wir hinzugefügt haben, aber mit diesem Hack können Sie tatsächlich sehen, wie Ruby unter der Haube funktioniert:
p developer.class.singleton_class.instance_methods false # [:backend]
Methoden mit „class_eval“ und „instance_eval“ definieren
Es gibt noch eine weitere Möglichkeit, eine Klassenmethode zu erstellen, und zwar die Verwendung von 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"
Dieses Stück Code wird vom Ruby-Interpreter im Kontext einer Instanz ausgewertet, die in diesem Fall ein Developer
-Objekt ist. Und wenn Sie eine Methode für ein Objekt definieren, erstellen Sie entweder eine Klassenmethode oder eine Singleton-Methode. In diesem Fall handelt es sich um eine Klassenmethode - genauer gesagt sind Klassenmethoden Singleton-Methoden, aber Singleton-Methoden einer Klasse, während die anderen Singleton-Methoden eines Objekts sind.
Andererseits wertet class_eval
den Code im Kontext einer Klasse statt einer Instanz aus. Es öffnet praktisch den Unterricht wieder. So kann class_eval
verwendet werden, um eine Instanzmethode zu erstellen:
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>"
Zusammenfassend lässt sich sagen, dass Sie beim Aufrufen der Methode class_eval
self
ändern, um auf die ursprüngliche Klasse zu verweisen, und wenn Sie instance_eval
aufrufen, ändert sich self
, um auf die Metaklasse der ursprünglichen Klasse zu verweisen.
Definieren fehlender Methoden im Handumdrehen
Ein weiteres Puzzleteil der Metaprogrammierung ist method_missing
. Wenn Sie eine Methode für ein Objekt aufrufen, geht Ruby zuerst in die Klasse und durchsucht ihre Instanzmethoden. Wenn es die Methode dort nicht findet, fährt es mit der Suche in der Vorfahrenkette fort. Wenn Ruby die Methode immer noch nicht findet, ruft es eine andere Methode namens method_missing
, die eine Instanzmethode von Kernel
ist, die jedes Objekt erbt. Da wir sicher sind, dass Ruby diese Methode irgendwann für fehlende Methoden aufrufen wird, können wir damit einige Tricks implementieren.
define_method
ist eine in der Module
-Klasse definierte Methode, die Sie verwenden können, um Methoden dynamisch zu erstellen. Um define_method
zu verwenden, rufen Sie es mit dem Namen der neuen Methode und einem Block auf, in dem die Parameter des Blocks zu den Parametern der neuen Methode werden. Was ist der Unterschied zwischen der Verwendung von def
zum Erstellen 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 Bereiche zu manipulieren, wenn Sie eine Klasse definieren, aber das ist eine ganz andere Geschichte. Schauen wir uns ein einfaches Beispiel an:
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!"
Dies zeigt, wie define_method
verwendet wurde, um eine Instanzmethode zu erstellen, ohne eine def
zu verwenden. Wir können jedoch noch viel mehr mit ihnen machen. Werfen wir einen Blick auf dieses Code-Snippet:
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"
Dieser Code ist nicht DRY, aber mit define_method
können wir ihn DRY machen:
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"
Das ist viel besser, aber immer noch nicht perfekt. Warum? 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 das beheben:
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
Dieses Stück Code ist ein wenig kompliziert, also lassen Sie es uns aufschlüsseln. Der Aufruf einer nicht existierenden Methode löst method_missing
. Hier wollen wir nur dann eine neue Methode erstellen, wenn der Methodenname mit "coding_"
beginnt. Andernfalls rufen wir einfach super an, um die Arbeit zu erledigen, eine Methode zu melden, die tatsächlich fehlt. Und wir verwenden einfach define_method
, um diese neue Methode zu erstellen. Das ist es! Mit diesem Stück Code können wir buchstäblich Tausende neuer Methoden erstellen, die mit "coding_"
beginnen, und diese Tatsache macht unseren Code DRY. Da define_method
für Module
privat ist, müssen wir es mit send
aufrufen.
Einpacken
Dies ist nur die Spitze des Eisbergs. Um ein Ruby Jedi zu werden, ist dies der Ausgangspunkt. Nachdem Sie diese Bausteine der Metaprogrammierung gemeistert und ihre Essenz wirklich verstanden haben, können Sie zu etwas Komplexerem übergehen, zum Beispiel Ihre eigene domänenspezifische Sprache (DSL) erstellen. DSL ist ein Thema für sich, aber diese grundlegenden Konzepte sind eine Voraussetzung für das Verständnis fortgeschrittener Themen. Einige der am häufigsten verwendeten Juwelen in Rails wurden auf diese Weise erstellt, und Sie haben wahrscheinlich seine DSL verwendet, ohne es zu wissen, wie z. B. RSpec und ActiveRecord.
Hoffentlich bringt Sie dieser Artikel dem Verständnis der Metaprogrammierung und vielleicht sogar dem Aufbau Ihrer eigenen DSL, mit der Sie effizienter codieren können, einen Schritt näher.