Skip to content

Türchen 23

Published: at 07:00 AMSuggest Changes

Decorators – Klassen flexibel erweitern

Mit Decorators (Dekoratoren) erhält JavaScript ein mächtiges Werkzeug, um Klassen oder deren Bestandteile gezielt zu verändern oder anzureichern, ohne den ursprünglichen Code direkt anzupassen. Die Idee ist, Metadaten, Logik oder Änderungen an Klassen, Methoden, Feldern oder Accessoren vorzunehmen, indem man eine spezielle Funktionsnotation verwendet, die direkt oberhalb der Deklaration notiert wird.

Einige von euch werden diese Möglichkeiten bereits aus TypeScript kennen. Mit der Umsetzung von ES2023 in die gängigen Browser wird es dann auch als direkter Standard in JavaScript nutzbar werden.

Wichtiger Hinweis: Die Decorators in JavaScript befinden sich zum Zeitpunkt dieses Artikels noch im Stadium eines TC39-Proposals (Stage 3). Das bedeutet, dass sie noch nicht vollständig standardisiert sind und in manchen Umgebungen nur mit speziellen Flags oder Transpilern (z. B. Babel, TypeScript) verfügbar sind. Dennoch lohnt sich ein Blick, um zu verstehen, wie sie gedacht sind.

Was sind Decorators?

Decorators sind Funktionen, die verwendet werden, um Klassen- oder Member-Definitionen zu modifizieren. Sie werden direkt vor der Klasse oder einem Klassen-Element (wie einer Methode oder einem Feld) notiert und können so die Definition beeinflussen, etwa:

Einfaches Beispiel: Klassen-Dekorator

Ein Klassen-Dekorator ist eine Funktion, die eine Klasse entgegennimmt und entweder verändert oder eine neue Klasse zurückgibt.

function sealed(constructor) {
  // Beispieldekorator, der das Erweitern der Klasse nach der Definition verhindert
  Object.seal(constructor);
  Object.seal(constructor.prototype);
}

@sealed
class Example {
  constructor(value) {
    this.value = value;
  }
}

const ex = new Example(10);
console.log(ex.value); // 10

// Versuch, die Klasse nachträglich anzupassen, schlägt fehl
try {
  Example.prototype.newProp = 'test';
} catch (err) {
  console.error('Klasse ist versiegelt:', err.message);
}

Hier wird die Klasse Example durch den @sealed-Dekorator modifiziert. Der Dekorator bewirkt, dass weder die Klasse noch ihr Prototyp nach der Deklaration verändert werden können.

Methoden dekorieren

Häufig möchte man einzelne Methoden mit Zusatzlogik versehen, etwa um Aufrufe zu protokollieren oder Zugriffsrechte zu prüfen. Dekoratoren für Methoden erhalten Information über das Zielobjekt, den Namen der Methode und deren ursprüngliche Descriptoren und können diese anpassen.

function logCall(_, context) {
  const originalMethod = context.descriptor.value;
  context.descriptor.value = function(...args) {
    console.log(`Aufruf von ${String(context.name)} mit Argumenten:`, args);
    return originalMethod.apply(this, args);
  };
}

class Calculator {
  @logCall
  add(a, b) {
    return a + b;
  }
}

const calc = new Calculator();
console.log(calc.add(3, 7)); 
// Ausgabe:
// Aufruf von add mit Argumenten: [ 3, 7 ]
// 10

In diesem Beispiel wird beim Aufruf von add automatisch ein Logging ausgeführt, bevor die ursprüngliche Logik der Methode aufgerufen wird. Der Dekorator ersetzt einfach die Methode durch eine neue Funktion, die zusätzliche Aufgaben erfüllt.

Felder und Accessoren dekorieren

Mit Decorators kann man auch Eigenschaften (Felder) oder Accessoren (Getter, Setter) dekorieren. Dies ist nützlich, um etwa Standardwerte durchzusetzen, Validierungen vorzunehmen oder private Felder zu manipulieren (sofern der Dekorator Zugriff erhält).

function readOnly(_, context) {
  if (context.kind === 'field') {
    return function (initialValue) {
      // Erlaube kein Überschreiben dieses Feldes nach dem ersten Setzen
      Object.defineProperty(this, context.name, {
        value: initialValue,
        writable: false,
        enumerable: true,
        configurable: false
      });
    }
  }
}

class User {
  @readOnly
  id = Math.floor(Math.random() * 1000);

  constructor(name) {
    this.name = name;
  }
}

const u = new User('Anna');
console.log(u.id); // z. B. 123
u.id = 999; // Keine Wirkung, da readOnly-Feld
console.log(u.id); // immer noch 123

Der Feld-Dekorator @readOnly sorgt hier dafür, dass das Feld id nur einmalig gesetzt wird und danach nicht mehr veränderbar ist.

Praxisszenarien für Decorators

Zum Schluss noch ein paar praktische Codebeispiele für den Einsatz von Decorators. Bitte beachte, dass Decorators sich aktuell (Stand ES) noch in der Proposal-Phase befinden und möglicherweise einen Transpiler wie Babel oder TypeScript erfordern, um im echten Code verwendet zu werden.

1. Logging und Debugging

Ein Methoden-Dekorator, der jeden Aufruf der Methode mit Zeitstempeln protokolliert und die Ausführungszeit misst.

function logExecution(_, context) {
  const originalMethod = context.descriptor.value;
  context.descriptor.value = function(...args) {
    const start = performance.now();
    console.log(`[LOG] Methode "${String(context.name)}" aufgerufen am: ${new Date().toISOString()}`);
    const result = originalMethod.apply(this, args);
    const end = performance.now();
    console.log(`[LOG] Ausführung von "${String(context.name)}" dauerte: ${end - start} ms`);
    return result;
  };
}

class DataService {
  @logExecution
  fetchData() {
    // Simulierter aufwendiger Prozess
    for (let i = 0; i < 1_000_000; i++) {}
    return { data: 'Beispiel' };
  }
}

const service = new DataService();
service.fetchData();
// Ausgabe:
// [LOG] Methode "fetchData" aufgerufen am: 2024-12-15T10:00:00.000Z
// [LOG] Ausführung von "fetchData" dauerte: XYZ ms

Hier wird ein Dekorator logExecution definiert, der die Ausführung einer Methode protokolliert. Der Dekorator nimmt zwei Argumente entgegen: _ (das Zielobjekt, das hier nicht verwendet wird) und context, das Informationen über die Methode enthält, die dekoriert wird.

Der Dekorator speichert die ursprüngliche Methode in originalMethod und überschreibt die Methode im context.descriptor.value mit einer neuen Funktion. Diese neue Funktion misst die Ausführungszeit der Methode und protokolliert den Start- und Endzeitpunkt sowie die Dauer der Ausführung.

Eine Instanz der DataService-Klasse wird erstellt und die fetchData-Methode aufgerufen. Die Konsolenausgabe zeigt, dass der Aufruf der Methode und die Dauer der Ausführung protokolliert werden.

Dieses Beispiel zeigt, wie Dekoratoren in JavaScript/TypeScript verwendet werden können, um zusätzliche Funktionalität zu Methoden hinzuzufügen, ohne deren ursprünglichen Code zu verändern. In diesem Fall wird die Ausführungszeit der Methode protokolliert, was nützlich für Debugging und Performance-Messungen sein kann.

2. Validierung und Sicherheit

Ein Methoden-Dekorator, der überprüft, ob ein Nutzer autorisiert ist, bevor die Methode ausgeführt wird. Falls nicht, wird ein Fehler geworfen.

function requireRole(requiredRole) {
  return function(_, context) {
    const originalMethod = context.descriptor.value;
    context.descriptor.value = function(user, ...args) {
      if (!user || user.role !== requiredRole) {
        throw new Error(`Zugriff verweigert: Nutzer hat nicht die erforderliche Rolle "${requiredRole}".`);
      }

      return originalMethod.apply(this, [user, ...args]);
    };
  };
}

class SecureService {
  @requireRole('admin')
  deleteRecord(user, recordId) {
    console.log(`Datensatz ${recordId} wird von ${user.name} gelöscht.`);
    // Löschlogik...
  }
}

const userAdmin = { name: 'Anna', role: 'admin' };
const userGuest = { name: 'Ben', role: 'guest' };

const secure = new SecureService();
secure.deleteRecord(userAdmin, 42); // Erfolg
// secure.deleteRecord(userGuest, 42); // Fehler: Zugriff verweigert
// secure.deleteRecord(userAdmin, 42); // deleteRecord wird ausgeführt

Hier wird ein Dekorator requireRole definiert, der sicherstellt, dass nur Benutzer mit einer bestimmten Rolle eine Methode ausführen können. Der Dekorator nimmt die erforderliche Rolle requiredRole als Argument entgegen und gibt eine Funktion zurück, die den Dekorator implementiert.

Dieses Beispiel zeigt, wie Dekoratoren in TypeScript verwendet werden können, um Zugriffskontrollen und Validierungen für Methoden zu implementieren, ohne deren ursprünglichen Code zu verändern.

3. Caching und Memoization

Ein Methoden-Dekorator, der die Ergebnisse teurer Methodenaufrufe speichert. Bei gleichen Argumenten liefert er beim nächsten Aufruf sofort den zwischengespeicherten Wert zurück, ohne die Methode erneut auszuführen.

function memoize(_, context) {
  const originalMethod = context.descriptor.value;
  const cache = new Map();

  context.descriptor.value = function(...args) {
    const key = JSON.stringify(args);
    if (cache.has(key)) {
      console.log(`Cache-Treffer für Argumente: ${key}`);
      return cache.get(key);
    }
    const result = originalMethod.apply(this, args);
    cache.set(key, result);
    return result;
  };
}

class MathService {
  @memoize
  expensiveCalculation(x, y) {
    // Simulierter teurer Rechenvorgang
    for (let i = 0; i < 1_000_000; i++) {}
    return x * y;
  }
}

const math = new MathService();
console.log(math.expensiveCalculation(10, 20)); // Berechnet neu
console.log(math.expensiveCalculation(10, 20)); // Liefert aus dem Cache

In diesem Codebeispiel wird ein Dekorator memoize definiert, der die Ergebnisse einer Methode zwischenspeichert (Memoisierung), um die Leistung zu verbessern. Der Dekorator nimmt zwei Argumente entgegen: _ (das Zielobjekt, das hier nicht verwendet wird) und context, das Informationen über die Methode enthält, die dekoriert wird.

Der Dekorator speichert die ursprüngliche Methode in originalMethod und erstellt einen cache als Map, um die Ergebnisse der Methode zu speichern.

Dieses Beispiel zeigt, wie Dekoratoren in TypeScript und demnächst auch in JavaScript verwendet werden können, um Memoisierung zu implementieren und die Leistung von Methoden zu verbessern, indem wiederholte Berechnungen vermieden werden.

Diese Beispiele verdeutlichen, wie Decorators dabei helfen können, Code flexibler, modularer und deklarativer zu gestalten, indem Logik für Logging, Sicherheitsprüfungen, Caching oder Konfiguration von der eigentlichen Methode getrennt wird.

Stand der Dinge

Die Decorator-Syntax ist noch nicht final standardisiert. Aktuell gibt es verschiedene Vorschläge und Implementierungen, zum Beispiel in TypeScript und Babel. In TypeScript sind Decorators bereits seit Längerem nutzbar (mit experimentalDecorators), sodass man einen Vorgeschmack darauf bekommt, wie der finale Standard aussehen könnte. Es ist aber möglich, dass sich die Syntax noch geringfügig ändert.

Fazit

Decorators machen Klassen und deren Bestandteile modularer, anpassungsfähiger und deklarativer. Statt Logik zum Beispiel in jede Methode hart einzucodieren, kannst du sie elegant durch einen Dekorator erweitern. Das fördert sauberen, wiederverwendbaren Code und ist ein großes Plus insbesondere in größeren Projekten oder Frameworks. Auch wenn Decorators noch nicht von allen JavaScript-Engines nativ unterstützt werden, lohnt es sich bereits jetzt, das Konzept kennenzulernen und auf kommende Sprachversionen gespannt zu sein.

Experimentiere mit Decorators in deinem nächsten Projekt und bringe Dein JavaScript KnowHow auf das nächste Level!


Previous Post
Türchen 24
Next Post
Türchen 22