Validierungsframework Mit Dekoratoren

Und Annotationen In TypeScript 1.5

TypeScript 1.5 führt neben der Modul-Grammatik von EcmaScript-6 und anderen netten Neuerungen auch das Konzept der Dekoratoren ein. Dabei handelt es sich um Funktionen, mit denen andere Sprachkonstrukte annotiert werden können. Zu diesen Konstrukten zählen Funktionen, Klassen aber auch Eigenschaften. JavaScript ruft die Dekorator-Funktionen beim Start eines Skripts auf und übergibt an diese die annotierten Konstrukte. Der Dekorator hat dann die Möglichkeit, diese Konstrukte zu erweitern oder durch andere Konstrukte zu ersetzen. Einen Überblick zu diesem Sprachkonstrukt, das auch in Angular 2 zum Einsatz kommen wird, findet man unter [1] und [2].
Dieser Beitrag demonstriert anhand der aktuell vorliegenden Alpha-Version von TypeScript den Einsatz von Dekoratoren am Beispiel eines einfachen Validierungsframeworks. Die Idee ist es, für Klassen sowie deren Eigenschaften mittels Annotationen Validierungsregeln festzulegen. Das nachfolgende Beispiel zeigt eine solche Klasse:

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

    @Required
    name: string;

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

    minPrice: number;
    maxPrice: number;
}

Im gezeigten Beispiel validiert die Annotation @Required ein Pflichtfeld und @MinValue sowie @MaxValue prüfen gegen eine festgelegte untere bzw. obere Schranke. Diese Schranken sowie die Fehlermeldungen nehmen sie als Parameter entgegen. Die Annotation @Validate wird im Gegensatz zu den anderen Annotationen auf eine Klasse angewandt. Sie erhält einen Lambda-Ausdruck für die Validierung und ebenfalls eine Fehlermeldung.
Zur Realisierung der Validierung soll jedes zu validierende Objekt ein Array __validators mit Validierungsfunktionen erhalten. Jede Validierungsfunktion prüft einen Aspekt des Objektes und retourniert im Fehlerfell eine Fehlermeldung; ansonsten liefert sie null zurück. Für diese Aufgabe nutzt das hier betrachtete Beispiel die Hilfs-Funktion addValidator:

function addValidator(target, fn, errorMessage) {

    var validationFn = (obj) => !fn(obj) ? errorMessage : null;

    if (!target.__validators) {
        target.__validators = [];
    }

    target.__validators.push(validationFn);

}

Diese Funktion nimmt das zu validierende Objekt, eine Funktion zum Validieren und eine eventuelle Fehlermeldung entgegen. Die übergebene Funktion liefert per Definition true, wenn die durchgeführte Validierung erfolgreich war; ansonsten false. Damit erstellt addValidator eine Validierungsfunktion, die im Fehlerfall die Fehlermeldung retourniert, und platziert diese in der Variable validationFn. Anschließend prüft addValidator, ob das Objekt bereits ein Array __validators aufweist und fügt dieses bei Bedarf hinzu. Danach hinterlegt sie die Validierungsfunktion in diesem Array.
Eine erste Version des Dekorators Required findet sich im nächsten Beispiel. Da dieser Dekorator auf Eigenschaften angewendet wird, bekommt er per Definition den jeweiligen Prototyp und den Namen der Eigenschaft übergeben. Er erstellt eine Funktion, welche ein Pflichtfeld validiert. Diese prüft, ob der zu prüfende Wert null oder undefined ist. Anschließend registriert Required diese Funktion zusammen mit einer einfachen Fehlermeldung als Validierungsfunktion. Dazu kommt die zuvor betrachtete Funktion addValidator zum Einsatz.

function Required(target, name) {
    var fn = (obj) => obj[name] !== null && typeof obj[name] !== "undefined";
    var errorMessage = name + " is required!";
    addValidator(target, fn, errorMessage);
}

Weniger schön am letzten Beispiel ist die Tatsache, dass die Fehlermeldung hartcodiert hinterlegt wurde. Das kann umgangen werden, indem man eine Funktion vorsieht, die den Dekorator erzeugt. Solche Factory-Funktionen können ebenso wie Dekoratoren in Form von Annotationen im Quellcode platziert werden. Das nachfolgende Beispiel demonstriert dies. Es zeigt zwei Factory-Funktionen, die Parameter zum Steuern der Validierung entgegen nehmen und jeweils eine Dekorator-Funktion mit der gewohnten Signatur retournieren:

function MinValue(min, errorMessage) {

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

function MaxValue(max, errorMessage) {
    return function (target, name) {
        var val = (obj) => obj[name] <= max;
        addValidator(target, val, errorMessage);
    };
}

Die Factory-Funktion für die Annotation Validate gestaltet sich ähnlich:

function Validate(fn, errorMessage) {
return function (target) {
addValidator(target.prototype, (obj) => fn(obj), errorMessage);
};
}

Zum Validieren nimmt sie einen Lambda-Ausdruck sowie eine Fehlermeldung entgegen. Der erzeugte Decorator nimmt wie gewohnt das betroffene Konstrukt entgegen. Da Validate zum Annotieren von Klassen gedacht ist, handelt es sich hierbei nicht um ein Objekt, sondern um die jeweilige Klasse bzw. Konstruktor-Funktion. Ein Parameter für den Namen der markierten Eigenschaft ist aus demselben Grund hinfällig. Damit addValidate die Validierungsfunktion zu sämtlichen aus der Klasse erzeugten Objekten hinzufügt, übergibt das betrachtete Beispiel den Prototyp dieser Klasse an addValidator.
Nun benötigt man nur noch ein Stück Code, welches sämtliche Validierungsfunktionen eines Objektes aufruft und die so erhaltenen Fehlermeldungen einsammelt. Das nächste Beispiel zeigt ein eine Klasse Validator, welche sich um diese Aufgabe kümmert:

class Validator {

    static validate(obj) {

        var errMessages = [];

        if (!obj || !obj.__validators) return errMessages;

        for (var fn of obj.__validators) {
            var errMessage = fn();

            if (errMessage) {
                errMessages.push(errMessage);
            }
        }

        return errMessages;
    }
}

Zum Testen der Implementierung kann man die eingangs gezeigte Klasse zum Beispiel instanziieren und an Validator.validate übergeben:

var hotel = new Hotel();
hotel.name = null;
hotel.ranking = 99;
hotel.minPrice = 120;
hotel.maxPrice = 80;

var err = Validator.validate(hotel);

if (err.length > 0) {
    alert(err.join("\n"));
}
else {
    alert("No errors!");
}

Links