Suche
Logging, Caching, Security mit Angular 2 und TypeScript: Teil 3

Querschnittsaspekte mit Angular 2 und TypeScript implementieren: AOP und Monkey Patching

Karsten Sitterberg

(c) Shutterstock / grmarc

Wie lassen sich Querschnittsaspekte wie Logging, Caching oder Security-Anforderungen in Angular-2-Webanwendungen umsetzen? Karsten Sitterberg stellt in dieser Serie verschiedene Ansätze vor und zeigt, wie sich diese langfristig auf die Wartbarkeit auswirken. Im dritten Teil geht es um Aspektorientierte Programmierung und das Konzept des „Monkey Patching.“

Serie: Querschnittsaspekte in Angular 2 mit TypeScript

Karsten SitterbergViele Softwareprojekte beinhalten Querschnittsaspekte wie Logging, Caching oder die Umsetzung von Sicherheitsanforderungen. In dieser Artikelserie wird die Anforderung, alle HTTP-Anfragen zu protokollieren, als Beispiel für einen Querschnittsaspekt gewählt, um verschiedene Ansätze zu illustrieren.

Unser Beispiel ist eine Anwendung, die die Entitäten Customer und Invoice verwaltet und diese durch ein HTTP API bezieht. Bei jedem Aufruf der HTTP API soll die URL protokolliert werden.
Angular 2 Tutorial

Aspektorientierte Programmierung (AOP)

Das Ziel aspektorientierter Programmierung (AOP) ist es, einen generischen Mechanismus bereitzustellen, um zusätzliche Logik in bestehenden Programmcode einzuweben, ohne den Quellcode anpassen zu müssen. Das erlaubt eine sehr lose Kopplung zwischen dem geschäftlichen Anwendungscode und den technischen Querschnittsfunktionen.

AOP definiert die folgenden Konzepte: Joinpoint, Pointcut, Advice und Aspect. Als kleine Einführung in AOP betrachten wir kurz die jeweiligen Zuständigkeiten.

Angular2 - Bild 5

Ein Aspect ist eine Klasse, die sogenannte Advices definiert und kapselt. Ein Advice ist die an einem bestimmten Punkt im Programm auszuführende Logik. Die Stellen, an denen ein Advice angewendet werden soll, werden Joinpoints genannt. Alle anzuwendenden Joinpoints werden über einen sogenannten Pointcut definiert.

Um eine AOP-basierte Lösung zu implementieren, wollen wir in diesem Artikel die Bibliothek aspect.js von Minko Gechev in Version 0.2.4 verwenden. Die Bibliothek nutzt die in ECMA Script 2015 vorgeschlagene Decorator-Syntax, um Metadaten zu einer Klasse zu definieren (ähnlich wie Annotationen in Java.) In TypeScript werden diese ebenfalls unterstützt und sind ein wesentlicher Baustein von Angular 2 bzw. dem Dependency-Injection Verfahren.

Angular2 - Bild 6

In aspect.js definiert jeder Aspect die Methoden (Advice), die an einer bestimmten Stelle im Programm (Pointcut) aufgerufen werden soll. In unserem Beispiel ist der Aspect der LoggingAspect. Dieser definiert einen Advice, der vor jedem Aufruf einer Methode, deren Namen mit „get“ beginnt und die zu einer InvoiceService- oder CustomerService-Klasse gehört, ausgeführt wird. Der Advice-Methode werden über das MetadataArgument zusätzlich die tatsächlichen Methoden- und Klassennamen sowie die Argumente der aufgerufenen Methode zur Verfügung gestellt.

logging.aspect.ts

import {Injectable} from '@angular/core';
import {beforeMethod, Metadata} from 'aspect.js/dist/lib/aspect';
@Injectable()
export class LoggingAspect {
   
 @beforeMethod({
   classNamePattern: /(Invoice|Customer)Service/,
   methodNamePattern: /^(get)/
 })
 invokeBeforeMethod(meta: Metadata) {
   console.log(`Inside of the logger.
     Called ${meta.className}.${meta.method.name}
     with args: ${meta.method.args.join(', ')}.`
   );
 }
}

Damit TypeScript Klassen für Aspekte als Ziel zur Verfügung stehen, müssen diese noch mit @Wove ausgezeichnet werden. Dies wird im folgenden Beispiel gezeigt:

invoice.service.ts

import {Injectable} from '@angular/core';
import {Http} from '@angular/http';
import {Observable} from 'rxjs/Observable';
import 'rxjs/Rx';
import {Wove} from 'aspect.js/dist/lib/aspect';
import {Invoice} from './invoice.model';
@Injectable()
@Wove()
export class InvoiceService{
 private url: string;
 constructor(private http: Http) {
   this.url = '/data/invoices/data.json';
 }
 get(): Observable<Invoice[]> {
   return this.http.get(this.url)
     .map(
       (response) => <Invoice[]>response.json()
     );
 }
}

Bei diesem Design wird in jedem Dienst jeweils URL-Ermittlung, HTTP-Behandlung und Mapping von Ergebnisdaten implementiert. Falls es sich dabei um gleichartige Implementierungen handelt, verletzen wir das DRY-Prinzip. Querschnittsfunktionalität jedoch ist sauber separiert: Hier wird DRY und SRP eingehalten.

Tests lassen sich bei diesem Ansatz auch leicht erstellen: Für die Querschnittsfunktionalität erstellt man eine zugehörige Testklasse zu den Aspekten, für den InvoiceService und den CustomerService ebenfalls je eine Testklasse. Auch bei Änderungen profitiert man von separierten Verantwortlichkeiten bzw. der Vermeidung von Wiederholungen, so dass lediglich Aspect und der zugehörige Test anzupassen sind. Um eine neue Querschnittsfunktionalität hinzuzufügen, wird ein neuer Aspect, z.B. CachingAspect, erzeugt. Auch hier ist der zugehörige Test einfach zu erstellen.

In der Bewertung schneidet diese Option gut ab: Mit Angular 2, TypeScript und aspect.js lässt sich AOP komfortabel umsetzen. Im Ergebnis erhält man gut testbaren, wartbaren und erweiterbaren Code. Für unerfahrene Entwickler ist jedoch mit etwas Einarbeitung in das Konzept und die praktische Anwendung zu rechnen. Anders als im Java-Umfeld sind die AOP-Bibliotheken im JavaScript-Bereich noch nicht so ausgereift. Minko Gechev hat mit aspect.js die am weitesten fortgeschrittene Library in Entwicklung, ein stabiles API ist jedoch noch nicht absehbar. Durch die erforderliche @Wove-Annotation wird die lose Kopplung, die man durch AOP normalerweise erzielt, etwas verringert.

Monkey Patching

Zum Abschluß betrachten wir ein Vorgehen, das im Umfeld dynamischer oder nicht typisierter Sprachen, wie Ruby und JavaScript, sehr beliebt ist: Monkey Patching. Dabei wird zur Laufzeit das bestehende Verhalten ersetzt, durchaus ähnlich zu AOP. Doch im Gegensatz zu AOP müssen die betreffenden Methoden explizit programmatisch ersetzt werden.

In unserem Beispiel wird die Logging-Anforderung implementiert, indem die getMethode des Http-Service durch eine eigene Implementierung ersetzt wird, die das Logging übernimmt und danach die eigentliche get-Methode aufruft.

Im Beispiel nutzen wir den Umstand aus, dass ein TypeScript Lambda-Ausdruck im Gegensatz zu JavaScript-Funktionen den aktuellen thisKontext nicht überschreibt und dieser weiter die umgebende Komponente ist. Das folgende Listing zeigt die AppComponent, in der die Http-get-Methode gepatcht wird:

@Component
 selector: 'app-cmp',
 templateUrl: '...',
 providers: [HTTP_PROVIDERS]
})
export class AppComponent {
 constructor(private http: Http) {
   let get = this.http.get;
   this.http.get = (url: string, options?: RequestOptionsArgs): Observable<Response> => {
     console.log(url);
     return get.call(this.http, url, options);
   };
 }
}

Die Implementierung von InvoiceService und CustomerService ist vergleichbar mit dem Decorator-basierten Design. Die Vor- und Nachteile bezüglich SRP und DRY spiegeln sich ebenso wider. Als zusätzlicher Nachteil kommt hinzu, dass erst zur Laufzeit aus der AppComponent heraus das Verhalten der Http-Klasse überschrieben wird, so dass es schwierig wird, dies zu testen.

Geht es um die Änderung des Logformats, so ist dies zumindest an einer Stelle durchzuführen – man muss diese nur finden.

Weitere Funktionalität kann analog ergänzt werden, jedoch sieht man schnell, dass die Anwendung unübersichtlich und schwer zu testen wird. Separiert man die Patches in unterschiedliche Klassen, muss man aufpassen, dass vorheriges Verhalten nicht unbeabsichtigt überschrieben wird.

Zu diesem Design kann man festhalten, dass es sich eher um ein Quick-and-Dirty Verfahren handelt, von dem man nicht zuletzt wegen der schlechten Testbarkeit und Wartbarkeit Abstand halten sollte.

Fazit

In Abhängigkeit von Kontext und den langfristigen Zielen stehen unterschiedliche Designansätze für die Implementierung von Querschnittsaspekten zur Verfügung. Auch die Kombination verschiedener Optionen kann sich anbieten: Ist beispielsweise das Mapping der Rückgabedaten immer gleichartig, so kann eine abstrakte Basisklasse oder die Delegation an eine Hilfsklasse eine gute Entscheidung sein.

Querschnittsaspekte sollten nicht mit Vererbung modelliert werden, da sich Einschränkungen ergeben, wenn mehrere unterschiedliche Anforderungen zu implementieren sind. Am vorteilhaftesten stellen sich hier Decorator-Pattern und AOP dar: Jede Anforderung kann in einer eigenen Klasse implementiert und separat getestet werden (SRP).

Wie sehr sich ein Ansatz auf Code-Duplizierung auswirkt, lässt sich darüber messen, wie oft Querschnittsfunktionen implementiert werden müssen. Im Beispiel gibt es zwei Dienste mit HTTP-Zugriff (Invoice und Customer). Haben diese beiden beispielsweise fünf Methoden (analog zu HTTP, get, put, post, delete, options), so muss die Anforderung, die URL zu protokollieren 5 x 2 = 10 Mal implementiert werden, wenn man den “simplen Ansatz” wählt. Beim Decorator-Muster muss jeder nur einmal für die fünf Methoden implementiert werden, also 5 x 1 = 5. Bei AOP wird jeder Aspect – dank regulärer Ausdrücke – nur einmal insgesamt implementiert, also 1 x 1 = 1.

Die folgende Tabelle fasst die Ergebnisse aller Abschnitte kurz zusammen. Zum einen wird angegeben, in welchen Klassen Funktionalität implementiert wird, zum anderen die zu erwartende Menge von doppeltem Code. Beispielsweise wird die Querschnittsfunktion “Protokollierung” in allen Klassen als Teil der Geschäftslogik bei dem simplen Ansatz umgesetzt. Die zu erwartende Menge an doppeltem Code bei “m” Querschnittsaspekten und “n” Diensten ist als Formel angegeben.

HTTP-Zugriff

Logging

Caching

Anzahl Implementierungen

Simples Vorgehen

Business class

Business class

Business class

 m x n

Überarbeiteter Ansatz

BackendService

BackendService

BackendService

m

Abstrakte Basisklasse

BaseClass

BaseClass

BaseClass

m

Decorator

Business class

Logging-
Decorator

Caching-
Decorator

m

AOP

Business class

LoggingAspect

CachingAspecto

1

Monkey Patching

Business class

AppComponent/ Beim Bootstrap

AppComponent/ Beim Bootstrap

m

.

Die meisten Überlegungen aus diesem Artikel finden analog Anwendung, wenn man andere Frameworks wie AngularJS oder React.js verwendet, auch wenn diese nicht zwangsläufig auf TypeScript aufbauen. Ganz gleich also, für was man sich entscheidet: Gutes Design ist Arbeit und fällt weder vom Himmel, noch gibt es einen goldenen Hammer, der alle Probleme löst.

Update: Seit Angular 4.3.1 existiert durch den neuen HttpClient die Möglichkeit, Interceptoren einzusetzen, um darüber Querschnittsaspekte abzubilden. Hinweise zu diesem Update und der Verwendung von Interceptoren finden sich hier: Abgefangen: So funktionieren Http-Interceptoren in Angular

Verwandte Themen:

Geschrieben von
Karsten Sitterberg
Karsten Sitterberg
Karsten Sitterberg ist als freiberuflicher Entwickler, Trainer und Berater für Java und Webtechnologien tätig. Karsten ist Physiker (MSc) und Oracle-zertifizierter Java Developer. Seit 2012 arbeitet er mit trion zusammen.
Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.