ECMAScript-2016-Dekoratoren in TypeScript

Kürzere Codestrecken mit ECMAScript-2016-Dekoratoren
Kommentare

Dekoratoren erlauben das Hinterlegen von Metainformationen in JavaScript-Quellcode. Somit lassen sich Codestrecken kürzer und ausdrucksstärker gestalten. Obwohl es sich dabei um einen Vorschlag für den Standard ECMAScript 2016 handelt, kann dieses neue Sprachkonstrukt bereits heute dank Technologien wie TypeScript genutzt werden.

In Mainstreamsprachen wie Java oder C# ist die Möglichkeit, Quellcode mit Metadaten zu annotieren, kaum mehr wegzudenken. Viele Frameworks nutzen diesen Mechanismus zur Unterstützung von kurzen und prägnanten sowie sich selbst erklärenden Codestrecken. Auch in JavaScript wird es solch ein Sprachkonstrukt geben. Hierbei ist die Rede von Dekoratoren, die mit dem JavaScript-Standard ECMAScript 2016 (ECMAScript 7) in die Sprache Einzug halten werden.

Der dafür vorliegende Vorschlag wird auch schon von Transpilern unterstützt, die moderne Sprachkonstrukte aus der Welt von ECMAScript 2015 und 2016 in handelsübliches JavaScript übersetzen. Zu diesen Transpilern gehört neben Babel auch TypeScript, das JavaScript zusätzlich um ein statistisches Typsystem erweitert. Aber auch populäre Frameworks, wie AngularJS 2.0 aus der Feder von Google, nutzen das künftige Dekoratorenkonzept intensiv und greifen dazu auf die erwähnten Transpiler zurück.

Der vorliegende Artikel geht auf die Möglichkeiten von Dekoratoren ein und zeigt anhand von Beispielen, die mit TypeScript 1.6 verfasst wurden, wie sich dieses Konzept anwenden lässt sowie was dahinter steckt. Die vollständigen Listings können unter auf GitHub heruntergeladen werden.

Dekoratoren in TypeScript
Da es sich bei Dekoratoren um ein Sprachmerkmal handelt, das erst mit ECMAScript 6 offiziell verabschiedet wird, stuft TypeScript 1.6 es als experimentell ein. Um die Unterstützung dafür zu aktivieren, ist bei direkter Nutzung des TypeScript-Compilers tsc der Vermerk experimentalDecorators in der Datei tsconfig.json zu hinterlegen:

{
  "compilerOptions": {
    "target": "ES5",
    "experimentalDecorators": true,
  }
}

Diese Datei wird im Stammverzeichnis der Anwendung erwartet und auch von Editoren mit TypeScript-Unterstützung, wie dem leichtgewichtigen Visual-Studio-Code, verwendet. Aus der Reihe tanzt hier leider derzeit noch das klassische Visual Studio, zumal es die Datei tsconfig.json ignoriert. Abhilfe schafft hier ein Eintrag in der Projektdatei. Informationen dazu findet man ebenfalls auf GitHub.

ECMAScript-2016-Dekoratoren in JavaScript

Als Einstieg in die Welt der Dekoratoren soll eine einfache Umsetzung betrachtet werden, die Teile des Programmcodes dokumentiert. Zur Vereinfachung beschränkt sich diese Dokumentation auf Konsolenausgaben.

Erste Schritte mit Dekoratoren

Listing 1 zeigt die Nutzung dieses Dekorators, der sich Docu nennt. Annotiert werden damit die Klasse Hotel, aber auch die Methode info sowie deren erstes Argument. Dabei ist zu beachten, dass die Möglichkeit zum Annotieren von Argumenten nicht durch den aktuell vorliegenden Vorschlag für ECMAScript 2016 abgedeckt wird, sondern eine Erweiterung durch TypeScript darstellt.

Listing 1
@Docu
class Hotel {
  constructor(name) {  […] }
  [...]
  @Docu
  info(@Docu preText) {
    return preText + this.name;
  }
}

Die Implementierung eines Dekorators besteht lediglich aus einer Funktion, die unter anderem das annotierte Sprachkonstrukt entgegennimmt. Diese Funktion hat die Möglichkeit, das Element zu verändern oder es durch ein anderes Element zu ersetzen. Somit lässt sich die Nutzung von Dekoratoren auch einfach nach ECMAScript 5 übersetzen:

class Hotel { […] }
Hotel = Docu(Hotel) || Hotel;

Liefert solch eine Funktion ein Objekt zurück, kommt dieses anstatt des annotierten Konstrukts zum Einsatz. Die Implementierung des hier genutzten Dekorators gestaltet sich wie folgt:

function Docu(target) {
  console.debug(target);
}

Diese Funktion nimmt die annotierte Klasse bzw. die damit einhergehende Konstruktorfunktion als target entgegen und gibt sie auf der Konsole aus. Dekoratoren, die Methoden annotieren, erhalten als target hingegen den Prototyp der Konstruktorfunktion. Daneben erhalten sie den Namen des Members als String (key) sowie den mit ECMAScript 5 eingeführten Property-Descriptor, der u. a. darüber informiert, ob ein Member zur Laufzeit überschrieben werden darf:

function Docu(target, key = null, descriptor = null) {
  console.debug(target);
  console.debug(key);
  console.debug(descriptor);
}

Die hier gezeigte Implementierung legt für die Argumente key und descriptor Standardwerte fest, damit TypeScript sie auch zum Annotieren von Klassen zulässt. Das ist notwendig, da beim Annotieren von Klassen die Dekoratorfunktion nur ein target erhält.

Eine Übersicht zu den Werten, die Dekoratoren übergeben bekommen, findet sich in Tabelle 1. Wie bereits weiter oben erwähnt, ist die Möglichkeit zum Annotieren von Argumenten eine Erweiterung von TypeScript.

Dekoratoren mit Parametern

Um das Verhalten von Dekoratoren anzupassen, können diese auch parametrisiert werden:

@DocuWithLabel("Repräsentiert ein Hotel")
class Hotel { […] }

Bei solchen Dekoratoren handelt es sich um Funktionen, die die genutzten Parameter entgegennehmen und eine Funktion retournieren, die auf das dekorierte Konstrukt angewandt wird. Diese Funktion erhält die in Tabelle 1 gezeigten Parameter. Mit Mitteln von ECMAScript 5 gestaltet sich das zuvor betrachtete Beispiel somit wie folgt:

class Hotel { […] }
Hotel = DocuWithLabel("Repräsentiert ein Hotel")(Hotel) || Hotel;

Die Implementierung des hier genutzten Dekorators findet sich in Listing 2. Sie nimmt die übergebene Beschreibung entgegen und liefert die Funktion zum Bearbeiten des gewünschten Konstrukts retour.

Listing 2
function DocuWithLabel(label) {
  
  return function(target, key = null, descriptor = null) {
    console.debug(label);
    console.debug(target);
    console.debug(key);
    console.debug(descriptor);
  }
}

Abfangen von Methodenaufrufen

Eine weitere Möglichkeit, die Dekoratoren bieten, ist das Abfangen von Methodenaufrufen. Dazu ersetzen sie eine annotierte Methode durch eine eigene, die ggf. an die ursprüngliche delegiert. Dieses Vorgehen wird auch als Monkeypatching bezeichnet. Zum Demonstrieren dieser Option nutzt Listing 3 den Dekorator Log, der jeden Aufruf der annotierten Methode auf der Konsole protokolliert.

Listing 3
class Hotel {
  [...]
  @Log
  info(@Docu preText): string {
    return preText + this.name;
  }
}

Die Implementierung von Log findet sich in Listing 4. Sie stellt zunächst sicher, dass ein Member und keine Klasse dekoriert wurde. In diesem Fall müssen die Parameter key und descriptor vorherrschen. Danach greift sie auf den Wert (value) des Descriptors zu. Da der Dekorator dem Annotieren von Methoden dienen soll, muss es sich dabei um eine Funktion, die die Methode repräsentiert, handeln. Anschließend weist Log dem Wert des Descriptors eine neue Funktion, welche die annotierte Funktion ersetzt, zu. Diese nimmt sämtliche übergebene Parameter über ihren Parameter params entgegen. Der mit ECMAScript 6 eingeführte, aus drei Punkten bestehende Restoperator macht das möglich.

Anschließend gibt die neue Funktion einen Protokolleintrag auf der Konsole aus und delegiert an die ursprüngliche Funktion. Da diese direkt, also ohne Angabe ihres Objekts, gerufen wird, kommt apply zum Einsatz. Diese Funktion nimmt den Wert von this, der innerhalb der gerufenen Methode vorherrschen soll, sowie ein Array mit Parametern entgegen und bringt die Funktion zur Ausführung.

Listing 4
function Log(target, key, descriptor) {
  
  if (!key || !descriptor) {
    console.error("Mit @Log dürfen nur Member dekoriert werden!");
    return;
  }

  var property = descriptor.value; // function
  
  if (!property || typeof property !== "function") {
    console.error("Mit @Log dürfen nur Methoden dekoriert werden!");
    return;
  }
  
  descriptor.value = function(...params) {
    console.debug('Calling ' + target.constructor.name + '.' + key);
    return property.apply(this, params);
  }
  
}

Validieren mit Dekoratoren

Um die Mächtigkeit von Dekoratoren zu demonstrieren, geht dieser Abschnitt mit einem abschließenden Beispiel auf die Erstellung eines einfachen Validierungsframeworks ein. Die Idee hinter diesem Beispiel ist es, Klassen und Member über Dekoratoren mit Informationen über nötige Validierungen zu bestücken. Listing 5 zeigt den Einsatz dieser Dekoratoren. Required validiert Pflichtfelder, MinValue und MaxValue prüfen, ob sich ein Wert in einem vorgegebenen Bereich befindet und Validate prüft mit einem zu übergebenden Lambda-Ausdruck, ob verschiedene Werte des Objekts miteinander korrelieren. Der von den meisten Methoden entgegengenommene String stellt eine ggf. auszugebende Fehlermeldung dar.

Listing 5
@Validate(h => h.minPrice <= h.maxPrice, "min < max")
class HotelInfo {

  @Required
  name: string;

  @MinValue(0, "Min: 3")
  @MaxValue(7, "Max: 7")
  ranking: number;

  minPrice: number;
  maxPrice: number;
}

Aus Platzgründen beschränken sich die Ausführungen hier auf den Dekorator MinValue (Listing 6), zumal sich die anderen sehr ähnlich gestalten. Der vollständige Quellcode online abgerufen werden. Dieser nimmt im Fall von MinValue die zu prüfende untere Schranke sowie eine Fehlermeldung entgegen und liefert die auf den Member anzuwendende Funktion zurück. Diese legt sich einen Lambda-Ausdruck zum Validieren der Eigenschaft zurecht, die durch den key referenziert wird. Er nimmt das zu validierende Objekt entgegen und retourniert einen booleschen Wert, der über den Ausgang der Validierung informiert. Anschließend delegiert MinValue an addValidator (Listing 7).

Listing 6
function MinValue(min, errorMessage) {

  return function (target, key) {
    var val = (obj) => obj[key] >= min;
    addValidator(target, val, errorMessage);
  };
}

Die Funktion addValidator richtet im übergebenen Prototyp ein Array __validators ein, sofern es noch nicht existiert. Auf diese Weise erweitert sie die annotierte Klasse bzw. die Klasse mit dem annotierten Member. Anschließend hinterlegt sie in diesem Array eine Validierungsfunktion, welche die übergebene Validierungsfunktion aufruft und im Fehlerfall die definierte Fehlermeldung retourniert. War die Validierung erfolgreich, liefert diese Validierungsfunktion null zurück.

Listing 7
function addValidator(target, validatorFn, errorMessage) {
  if (!target.__validators) {
    target.__validators = [];
  }
  var extFn = (obj) => !validatorFn(obj) ? errorMessage : null;
  target.__validators.push(extFn);
}

Zum Validieren eines Objekts muss man nun lediglich sämtliche Funktionen der Eigenschaft __validators aufrufen und die so erhaltenen Fehlermeldungen einsammeln. Eine Hilfsfunktion dafür findet sich im Quellcode online. Diese nimmt das zu validierende Objekt entgegen und liefert sämtliche ermittelte Fehlermeldungen als Array mit Strings zurück:

var err = Validator.validate(hotel);

Fazit

Dekoratoren eröffnen ähnlich wie Annotationen in Java oder Attribute in .NET die Möglichkeit, Quellcode mit Metainformationen zu versehen. Dies führt zu kürzeren und prägnanteren Codestrecken. Im Gegensatz zu den genannten Sprachkonstrukten in Java und .NET basieren Dekoratoren jedoch lediglich auf Funktionen. Das macht dieses Konzept leichtgewichtig und erlaubt eine einfache Kompilierung für ältere JavaScript-Versionen. Allerdings erhöht diese Designentscheidung auch die Mächtigkeit des Dekoratorkonzepts, zumal die genutzten Funktionen die annotierten Klassen und Methoden nach Belieben erweitern, abändern oder sogar durch andere Klassen und Methoden ersetzen können.

Auch wenn es sich bei Dekoratoren um ein künftiges Sprachmerkmal handelt, das für ECMAScript 2016 vorgesehen ist, kann es dennoch heute schon als eine Art De-facto-Standard betrachtet werden. Der Grund dafür ist, dass es sowohl populäre Transpiler, wie Babel oder TypeScript, unterstützen sowie populäre Frameworks, wie AngularJS 2.0, exzessiv einsetzen. Aber auch bestehende Frameworks, wie AngularJS 1.x, können von Dekoratoren profitieren, um wiederkehrende Aufgaben zu vereinfachen. Informationen dazu finden sich beispielsweise hier.

Schnell und überall: Datenzugriff mit Entity Framework Core 2.0

Dr. Holger Schwichtenberg (www.IT-Visions.de/5Minds IT-Solutions)

C# 7.0 – Neues im Detail

Christian Nagel (CN innovation)

Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -