Monkey Patching in dynamischen Programmiersprachen: Ein JavaScript-Beispiel

Javascript
Dynamische Programmierung
Monkey Patching in dynamischen Programmiersprachen cover image

Einleitung

Dieser Artikel befasst sich mit den Konzepten der dynamischen und statischen Programmiersprachen, den Hauptunterschieden zwischen den beiden und den Vorteilen und Fallstricken, die jedes Paradigma bietet. 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 uns mit einigen Schlüsselbegriffen vertraut machen, die in diesem Zusammenhang häufig verwendet werden: Kompilierzeit, Laufzeit und Typenprüfung.

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

Kompilierzeit

Die Kompilierzeit ist der erste Schritt im Lebenszyklus eines Programms. Ein Entwickler schreibt Code in einer bestimmten Programmiersprache. In den meisten Fällen ist die Maschine nicht in der Lage, den in einer Hochsprache geschriebenen Code zu verstehen, so dass ein spezieller Compiler verwendet wird, um ihn in ein niedrigeres Zwischenformat zu übersetzen, das dann zur Ausführung bereit ist.

Laufzeit

Die Laufzeit umfasst in der Regel zwei Schritte: das Laden des Programms in den Speicher durch Zuweisung der für seine Ausführung erforderlichen Ressourcen zusammen mit seinen Anweisungen und die anschließende Ausführung des Programms in der Reihenfolge dieser Anweisungen.

Das folgende Diagramm veranschaulicht diesen Prozess:

Typprüfung

Die Typprüfung ist eine eingebaute Funktion in fast allen Programmiersprachen. Es handelt sich dabei um die Fähigkeit zu prüfen, ob ein einer bestimmten Variablen zugewiesener Wert dem richtigen Typ dieser Variablen entspricht. Jede Programmiersprache hat eine andere Art, einen Wert eines bestimmten Typs im Speicher darzustellen. Diese unterschiedlichen Darstellungen ermöglichen es, die Übereinstimmung zwischen dem Typ eines Wertes und dem Typ einer Variablen zu überprüfen, der Sie diesen Wert zuweisen wollen.

Nachdem wir nun ein grundlegendes Verständnis des Lebenszyklus eines Programms und der Typüberprüfung haben, können wir uns nun mit statischen Programmiersprachen beschäftigen.

Statische Programmiersprachen

Statische Programmiersprachen, die auch als statisch typisierte Sprachen bezeichnet werden, sind Sprachen, die die erwähnte Typüberprüfung in der Kompilierungsphase anwenden. Das bedeutet, dass eine Variable ihren Typ aus der Deklaration beibehält und ihr kein Wert zugewiesen werden kann, der nicht dem Typ ihrer Deklaration entspricht. Statische Programmiersprachen bieten zusätzliche Sicherheit beim Umgang mit Typen, können aber den Entwicklungsprozess in bestimmten Anwendungsfällen verlangsamen, wenn dies zu einer harten Einschränkung wird.

Dynamische Programmiersprachen

Dynamische Programmiersprachen hingegen führen eine Typüberprüfung zur Laufzeit durch. Das bedeutet, dass jede Variable an jedem Punkt des Programms einen beliebigen Wert annehmen kann. Dies kann von Vorteil sein, da es dem Entwickler ein Maß an Flexibilität bietet, das in statischen Sprachen nicht vorhanden ist. Dynamische Sprachen sind in der Regel bei der Ausführung langsamer als ihre statischen Gegenstücke, da sie einen zusätzlichen Schritt zur dynamischen Ermittlung der Typisierung jeder Variablen beinhalten.

Monkey Patch

Statische vs. dynamische Typisierung ist ein grundlegendes Merkmal einer Programmiersprache. Die Entscheidung für das eine oder andere Paradigma kann eine Vielzahl unterschiedlicher Muster und Praktiken ermöglichen, die die Qualität und Geschwindigkeit der Entwicklung erheblich verbessern können. Es kann aber auch die Tür für viele Einschränkungen und Anti-Patterns öffnen, wenn keine sorgfältigen Überlegungen bei den Designentscheidungen angestellt werden.

Insbesondere dynamisch typisierte Programmiersprachen sind dafür bekannt, dass sie ein höheres Maß an Flexibilität bieten, da sie eine Variable nicht auf einen einzigen Typ beschränken. Diese Flexibilität geht mit einer zusätzlichen Verantwortung des Entwicklers einher, wenn er Programme implementiert und debuggt, um sicherzustellen, dass kein unvorhersehbares Verhalten auftritt. Das Monkey-Patch-Muster geht auf diese Philosophie zurück.

Monkey Patch bezieht sich auf den Prozess der Erweiterung/Änderung der Funktionsweise einer Komponente während der Laufzeit. Die betreffende Komponente kann eine Bibliothek, eine Klasse, eine Methode oder sogar ein Modul sein. Die Idee ist dieselbe: Ein Teil des Codes soll eine bestimmte Aufgabe erfüllen, und das Ziel des Monkey Patching ist es, das Verhalten dieses Teils des Codes so zu ändern oder zu erweitern, dass er eine neue Aufgabe erfüllt, ohne den Code selbst zu ändern.

Dies ist in der dynamischen Programmiersprache möglich, da unabhängig von der Art der Komponente, mit der wir es zu tun haben, diese immer noch die gleiche Struktur eines Objekts mit verschiedenen Attributen hat. Die Attribute können Methoden enthalten, die neu zugewiesen werden können, um ein neues Verhalten des Objekts zu erreichen, ohne dass wir uns mit seinen Interna und Implementierungsdetails befassen müssen. Dies ist besonders bei Bibliotheken und Modulen von Drittanbietern nützlich, da diese in der Regel schwieriger zu verändern sind.

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

Beispiel

Implementierung eines minimalen Test-Frameworks mit dem nativen HTTP-Modul von Node

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

Das folgende Beispiel ist eine einfache Implementierung von Monkey-Patching für das native http-Modul von NodeJs. Das http-Modul ist die Schnittstelle, die die http-Protokollmethoden für NodeJs implementiert. Es wird hauptsächlich verwendet, um Barebone http-Server zu erstellen und mit externen Diensten über das http-Protokoll zu kommunizieren.

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 so, dass sie nur das erwartete Ergebnis zurückgibt, nämlich ein Array mit zufälligen Benutzerkennungen. Dies scheint nicht sehr wichtig zu sein, da wir nur Daten abrufen, aber wenn wir einen anderen Testfall implementieren, der die Änderung von Daten beinhaltet, könnten wir bei der Ausführung 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, während wir die Sicherheit unserer Produktionsdienste 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"]
  };
}

// Beispiel-Testsuite, Aufruf der neuen gepatchten get-Methode zum Testen
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 einfach: 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 innerhalb des Testfalls 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 Beschränkungen

Es sollte nicht überraschen, dass Monkey-Patching seine eigenen Fehler und Einschränkungen hat. Im Kontext von Modulen im Node-Modulsystem wird das Patchen eines globalen Moduls wie http als eine Operation mit Seiteneffekten betrachtet, da http von jedem Punkt innerhalb der Codebasis zugänglich ist und jede andere Entität eine Abhängigkeit davon haben könnte. Diese Entitäten erwarten, dass das http-Modul in seinem gewohnten Verhalten arbeitet. Wenn wir eine der http-Methoden ändern, brechen wir effektiv alle anderen http-Abhängigkeiten innerhalb der Codebasis.

Da wir in einer dynamisch typisierten Sprache arbeiten, kann es sein, dass die Dinge nicht sofort scheitern, sondern zu einem unvorhersehbaren Verhalten übergehen, was die Fehlersuche zu einer äußerst komplexen Aufgabe macht. In anderen Anwendungsfällen könnte es zwei verschiedene Patches derselben Komponente für dasselbe Attribut geben, wobei wir nicht wirklich vorhersagen können, welcher Patch Vorrang vor dem anderen hat, was zu einem noch unvorhersehbareren Code führt.

Es ist auch wichtig zu erwähnen, dass Monkey-Patching in verschiedenen Programmiersprachen leichte Unterschiede im Verhalten aufweisen kann. Es hängt alles vom Design der Sprache und der Wahl der Implementierung ab. In Python zum Beispiel sind nicht alle Instanzen, die eine gepatchte Methode verwenden, von dem Patch betroffen. Wenn eine Instanz die gepatchte Methode explizit aufruft, dann erhält sie die neue, aktualisierte Version, im Gegensatz dazu erhalten andere Instanzen, die vielleicht nur Attribute haben, die auf die gepatchte Methode zeigen und sie nicht explizit aufrufen, die ursprüngliche Version, dies liegt daran, wie die Pythonbindung in Klassen funktioniert.

Fazit

In diesem Artikel haben wir uns mit den Unterschieden zwischen statischen und dynamischen Programmiersprachen beschäftigt. Wir haben gesehen, wie dynamische Programmiersprachen von neuen Paradigmen und Mustern profitieren können, die die inhärente Flexibilität dieser Sprachen nutzen. Das Beispiel, das wir vorgestellt haben, bezog sich auf das Monkey-Patching, eine Technik, mit der das Verhalten von Code erweitert werden kann, ohne ihn im Quellcode zu ändern. Wir sahen einen Fall, in dem der Einsatz dieser Technik von Vorteil war, aber auch ihre potenziellen Nachteile. Bei der Softwareentwicklung geht es um Kompromisse, und die richtige Lösung für ein Problem zu finden, erfordert von den Entwicklern sorgfältige Überlegungen und ein gutes Verständnis der Architekturprinzipien und -grundlagen.


Career Services background pattern

Karrieredienste

Contact Section background image

Lass uns in Kontakt bleiben

Code Labs Academy © 2024 Alle Rechte vorbehalten.