Monkey Patching in dynamischen Programmiersprachen: Ein JavaScript-Beispiel

Aktualisiert auf November 15, 2024 8 Minuten gelesen

Monkey Patching in dynamischen Programmiersprachen: Ein JavaScript-Beispiel cover image

Einführung

In diesem Artikel werden die Konzepte dynamischer und statischer Programmiersprachen, die Hauptunterschiede zwischen beiden und die Vorteile und Fallstricke jedes Paradigmas untersucht. Diese Untersuchung wird sich weiter auf dynamische Programmiersprachen konzentrieren, insbesondere auf eines der wesentlichen Muster, die sie ermöglichen: Monkey Patch. Dieses Muster wird anhand eines Beispiels in JavaScript vorgestellt.

Dynamische vs. statische Programmiersprachen

Terminologie

Um zu verstehen, was eine dynamische oder eine statische Sprache ausmacht, müssen wir einige Schlüsselbegriffe verstehen, die in diesem Zusammenhang häufig verwendet werden: Kompilierungszeit, Laufzeit und *Typprüfung *.

Kompilierung und Laufzeit sind zwei Begriffe, die sich auf verschiedene Phasen im Lebenszyklus eines Computerprogramms beziehen, beginnend mit der Kompilierungszeit.

Kompilierungszeit

Die Kompilierungszeit ist der erste Schritt im Lebenszyklus eines Programms. Ein Entwickler schreibt Code in einer bestimmten Programmiersprache. Meistens ist die Maschine nicht in der Lage, den in einer höheren Sprache geschriebenen Code zu verstehen, sodass ein spezieller Compiler verwendet wird, um ihn in ein Zwischenformat einer niedrigeren Ebene zu übersetzen, das zur Ausführung bereit ist.

Laufzeit

Die Laufzeit umfasst normalerweise zwei Schritte: Laden des Programms in den Speicher durch Zuweisen der für seine Ausführung erforderlichen Ressourcen zusammen mit seinen Anweisungen und anschließendes Ausführen des Programms gemäß der Reihenfolge dieser Anweisungen.

Das folgende Diagramm veranschaulicht diesen Prozess:

Typprüfung

Die Typprüfung ist eine integrierte Funktion in fast allen Programmiersprachen. Dabei handelt es sich um die Möglichkeit zu überprüfen, ob ein einer bestimmten Variablen zugewiesener Wert dem richtigen Typ dieser Variablen entspricht. Jede Programmiersprache hat eine andere Möglichkeit, einen Wert eines bestimmten Typs im Speicher darzustellen. Diese unterschiedlichen Darstellungen ermöglichen es, die Übereinstimmung zwischen dem Typ eines Werts und dem Typ einer Variablen zu überprüfen, der Sie diesen Wert zuweisen möchten.

Nachdem wir nun ein umfassendes Verständnis des Programmlebenszyklus und der Typprüfung haben, können wir mit der Erforschung statischer Programmiersprachen fortfahren.

Statische Programmiersprachen

Statische Programmiersprachen, auch statisch typisierte Sprachen genannt, sind Sprachen, die die Typprüfung anwenden, die wir in der Kompilierungsphase erwähnt haben. Dies bedeutet effektiv, dass eine Variable ihren Typ aus der Deklaration behält und ihr kein anderer Wert als Werte aus ihrem Deklarationstyp zugewiesen werden kann. Statische Programmiersprachen bieten zusätzliche Sicherheit beim Umgang mit Typen, können jedoch in bestimmten Anwendungsfällen den Entwicklungsprozess verlangsamen, wenn dies zu einer starken Einschränkung wird.

Dynamische Programmiersprachen

Dynamische Programmiersprachen hingegen wenden die Typprüfung zur Laufzeit an. Das bedeutet, dass jede Variable an jedem Punkt im Programm einen beliebigen Wert enthalten kann. Dies kann von Vorteil sein, da es dem Entwickler ein Maß an Flexibilität bietet, das in den statischen Sprachen nicht vorhanden ist. Dynamische Sprachen sind tendenziell langsamer in der Ausführung als ihre statischen Gegenstücke, da sie einen zusätzlichen Schritt erfordern, bei dem die Typisierung jeder Variablen dynamisch ermittelt wird.

Affen-Patch

Statische vs. dynamische Typisierung ist ein grundlegendes Merkmal einer Programmiersprache. Der Einsatz eines Paradigmas gegenüber dem anderen kann eine Vielzahl unterschiedlicher Muster und Praktiken ermöglichen, die die Qualität und Geschwindigkeit der Entwicklung erheblich verbessern können. Es kann auch die Tür für viele Einschränkungen und Anti-Patterns öffnen, wenn bei Designentscheidungen keine sorgfältigen Überlegungen angestellt werden.

Insbesondere dynamisch typisierte Programmiersprachen bieten bekanntermaßen ein höheres Maß an Flexibilität, da sie eine Variable nicht auf einen einzelnen Typ beschränken. Diese Flexibilität geht mit zusätzlichen Kosten für den Entwickler einher, wenn er Programme implementiert und debuggt, um sicherzustellen, dass kein unvorhersehbares Verhalten auftritt. Das Affen-Patch-Muster stammt aus dieser Philosophie.

Unter Monkey Patch versteht man den Prozess der Erweiterung/Änderung der Funktionsweise einer Komponente zur Laufzeit. Bei der betreffenden Komponente kann es sich um eine Bibliothek, eine Klasse, eine Methode oder sogar ein Modul handeln. Die Idee ist dieselbe: Ein Codeteil wird erstellt, um eine bestimmte Aufgabe zu erfüllen, und das Ziel des Monkey-Patchings besteht darin, das Verhalten dieses Codeteils so zu ändern oder zu erweitern, dass er eine neue Aufgabe erfüllt, ohne den Code selbst zu ändern .

Dies wird in der dynamischen Programmiersprache ermöglicht, da sie unabhängig von der Art der Komponente, mit der wir es zu tun haben, immer noch die gleiche Struktur eines Objekts mit unterschiedlichen Attributen aufweist. Die Attribute können Methoden enthalten, die neu zugewiesen werden können, um ein neues Verhalten im Objekt zu erreichen ohne auf dessen Interna und Einzelheiten der Umsetzung einzugehen. Dies ist besonders nützlich bei Bibliotheken und Modulen von Drittanbietern, da diese tendenziell schwieriger zu optimieren sind.

Das folgende Beispiel zeigt einen häufigen Anwendungsfall, der von der Verwendung der Monkey-Patch-Technik profitieren kann. Zur Implementierung wurde hier Javascript verwendet, dies sollte jedoch im Großen und Ganzen auch für jede andere dynamische Programmiersprache gelten.

Beispiel

Implementieren Sie ein minimales Test-Framework mit dem nativen HTTP-Modul von Node

Unit- und Integrationstests können unter die Anwendungsfälle des Monkey-Patchings fallen. Dabei handelt es sich in der Regel um Testfälle, die sich für Integrationstests über mehr als einen Dienst erstrecken, oder um API- und/oder Datenbankabhängigkeiten für Unit-Tests. In diesen beiden Szenarien und um die Testziele überhaupt zu erreichen, möchten wir, dass unsere Tests unabhängig von diesen externen Ressourcen sind. Der Weg, dies zu erreichen, ist durch Spott. Beim Mocking wird das Verhalten externer Dienste simuliert, damit sich der Test auf die tatsächliche Logik des Codes konzentrieren kann. Monkey-Patching kann hier hilfreich sein, da es die Methoden der externen Dienste ändern kann, indem es sie durch Platzhaltermethoden ersetzt, die wir „Stub“ nennen. Diese Methoden geben in den Testfällen das erwartete Ergebnis zurück, sodass wir vermeiden können, nur zu Testzwecken Anfragen an Produktionsdienste zu stellen.

Das folgende Beispiel ist eine einfache Implementierung des Monkey-Patchings auf dem nativen http-Modul von NodeJs. Das http-Modul ist die Schnittstelle, die die http-Protokollmethoden für NodeJs implementiert. Es wird hauptsächlich zum Erstellen von Barebone-HTTP-Servern und zur Kommunikation mit externen Diensten über das HTTP-Protokoll verwendet.

Im folgenden Beispiel haben wir einen einfachen Testfall, bei dem wir einen externen Dienst aufrufen, um die Liste der Benutzer-IDs abzurufen. Anstatt den eigentlichen Dienst aufzurufen, patchen wir die http-get-Methode, sodass sie lediglich das erwartete Ergebnis zurückgibt, das ein Array zufälliger Benutzer-IDs ist. Das scheint vielleicht nicht so wichtig zu sein, da wir nur Daten abrufen, aber wenn wir einen anderen Testfall implementieren, der die Änderung von Daten in irgendeiner Form beinhaltet, könnten wir beim Ausführen von Tests versehentlich Daten in der Produktion ändern.

Auf diese Weise können wir unsere Funktionalitäten implementieren und Tests für jede Funktionalität schreiben und gleichzeitig die Sicherheit unserer Produktionsdienstleistungen gewährleisten.

// import the http module
let http = require("http");

// patch the get method of the http module
http.get = async function(url) {
  return {
    data: ["1234", "1235", "1236", "1236"]
  };
}

// example test suite, call new patched get method for testing
test('get array of user ids from users api', async () => {
  const res = await http.get("https://users.api.com/ids");
  const userIds = res.data;
  expect(userIds).toBeDefined();
  expect(userIds.length).toBe(4);
  expect(userIds[0]).toBe("1234");
});

Der obige Code ist unkompliziert: Wir importieren das http-Modul und weisen der http.get-Methode eine neue Methode zu, die lediglich ein Array von IDs zurückgibt. Jetzt rufen wir die neue gepatchte Methode im Testfall auf und erhalten das neue erwartete Ergebnis.

~/SphericalTartWorker$ npm test

> nodejs@1.0.0 test
> jest

PASS  ./index.test.js
 get array of user ids from users api (25 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        0.977 s, estimated 2 s
Ran all test suites.

Häufige Fallstricke und Einschränkungen

Es sollte nicht überraschen, dass das Patchen von Affen seine eigenen Mängel und Einschränkungen aufweist. Im Zusammenhang mit Modulen im Knotenmodulsystem wird das Patchen eines globalen Moduls wie http als Vorgang mit Nebenwirkungen betrachtet, da auf http von jedem Punkt innerhalb der Codebasis aus zugegriffen werden kann und jede andere Entität möglicherweise von ihr abhängig ist. Diese Entitäten erwarten, dass das http-Modul in seinem üblichen Verhalten arbeitet. Durch die Änderung einer der http-Methoden brechen wir effektiv alle anderen http-Abhängigkeiten innerhalb der Codebasis.

Da wir mit einer dynamisch typisierten Sprache arbeiten, kann es sein, dass die Dinge nicht sofort fehlschlagen, sondern dass es eher zu einem unvorhersehbaren Verhalten kommt, was das Debuggen zu einer äußerst komplexen Aufgabe macht. In anderen Anwendungsfällen gibt es möglicherweise zwei verschiedene Patches derselben Komponente für dasselbe Attribut. In diesem Fall können wir nicht wirklich vorhersagen, welcher Patch Vorrang vor dem anderen haben wird, was zu einem noch unvorhersehbareren Code führt.

Es ist auch wichtig zu erwähnen, dass das Patchen von Affen zwischen verschiedenen Programmiersprachen zu geringfügigen Verhaltensunterschieden führen kann. Es hängt alles vom Sprachdesign und den Implementierungsoptionen ab. In Python sind beispielsweise nicht alle Instanzen, die eine gepatchte Methode verwenden, von dem Patch betroffen. Wenn eine Instanz die gepatchte Methode explizit aufruft, erhält sie die neue aktualisierte Version. Im Gegensatz dazu erhalten andere Instanzen, deren Attribute möglicherweise nur auf die gepatchte Methode verweisen und diese nicht explizit aufrufen, die Originalversion. Dies liegt an der Funktionsweise von Python Bindung in Klassen funktioniert.

Abschluss

In diesem Artikel haben wir die allgemeinen Unterschiede zwischen statischen und dynamischen Programmiersprachen untersucht und gesehen, wie dynamische Programmiersprachen von neuen Paradigmen und Mustern profitieren können, indem sie die inhärente Flexibilität dieser Sprachen nutzen. Das von uns vorgestellte Beispiel bezog sich auf Monkey-Patching, eine Technik, mit der das Verhalten von Code erweitert wird, ohne ihn gegenüber der Quelle zu ändern. Wir haben einen Fall gesehen, in dem der Einsatz dieser Technik neben möglichen Nachteilen auch Vorteile bieten würde. Bei der Softwareentwicklung geht es vor allem um Kompromisse, und um die richtige Lösung für das Problem zu finden, sind vom Entwickler ausführliche Überlegungen und ein gutes Verständnis der Architekturprinzipien und -grundlagen erforderlich.


Machen Sie Ihre Karriere zukunftssicher, indem Sie sich im Web Development Bootcamp von Code Labs Academy in HTML, CSS und JavaScript weiterbilden.