Typischerweise wird in der Softwareentwicklung eine PULL basierte Architektur verwendet und nur selten eine PUSH Architektur. Um Daten von einem Server anzufragen wird ein Service aufgerufen, welcher anschließend einen Request an den Server schickt und die benötigten Daten erhält. Während dieses Prozesses ist der Main UI Thread aber blockiert.
Angular selbst überprüft regelmäßig das View Model auf Änderungen. Sollte sich aufgrund einer State Änderung das View Model verändern, rendert Angular die View neu, weil Angular automatisch das UI anpasst, wenn sich das Model verändert. Durch diese kontinuierlichen Beobachtungen wird die ganze Anwendung langsamer.
Zudem steigt mit dem traditionellen Pull Ansatz die Komplexität. Besonders, wenn mehrere Views dieselben Daten benötigen. Was passiert, wenn sich die Daten ändern? Wie werden die Views darüber informiert, dass neue Daten verfügbar sind? Callback Funktionen könnten helfen, aber dann wird die Anwendung schnell unübersichtlich. Außerdem wirken sie sich besonders bei größeren Anwendungen negativ auf die Handhabung und Wartung aus. Schließlich müssen die Objekte einzeln initialisiert werden und die Abhängigkeiten bzw. die Reihenfolge der Methoden beachtet werden.
Um dieses Problem zu umgehen kann eine Push basierte Architektur genutzt werden. Angular bietet hier mit RxJS und dem Fassaden Entwurfsmuster eine hervorragende Möglichkeit zur Implementierung! Die Implementierung wird anhand eines Angular Beispiels in diesem Blog Artikel erläutert und ist für alle Plattformen und Frameworks (Angular, React, Vue.js, usw.) übertragbar.
Liste der Komponenten:
- Entwicklungsumgebung
- Angular CLI
Was ist eine Fassade eigentlich?
Das Fassaden Entwurfsmuster ist ein zentrales Interface, welches mit verschiedenen Schnittstellen eines oder mehrerer untergeordneter Systeme kommuniziert. Es kann je nach Bedarf zusätzliche Funktionen sowohl vor als auch nach einer Client Anfrage ausführen.
Das Fassaden Objekt bietet eine einheitliche und meist vereinfachte Schnittstelle zu einer Menge von Schnittstellen eines Subsystems.
Es sorgt als Vermittler dafür, dass die Kommunikation bzw. der Zugriff auf die einzelnen Komponenten eines Subsystems vereinfacht und damit auch die direkte Abhängigkeit von diesen Komponenten minimiert wird. Es delegiert die Clientaufrufe so, dass Clients weder die Klassen noch ihre Beziehungen und Abhängigkeiten kennen müssen.
Das ist besonders in großen Systemen hilfreich, wenn ein Subsystem viele technisch orientierte Klassen enthält, die selten von außen verwendet werden. Die Verwendung einer Fassade senkt die Komplexität, da mehrere Schnittstellen zu einer zusammengefasst werden. Außerdem kann das Subsystem durch die lose Kopplung leichter erweitert werden.
Die PUSH Architektur
Für eine Push basierte Architektur können fortgeschrittene Entwurfsmuster wie Redux oder NgRx verwendet werden. Allerdings kann auch eine sehr elegante und performante push basierte Lösung mit RxJS erreicht werden.
Mit Hilfe der lang lebigen RxJS Observable streams können Veränderungen der Daten an alle Subscriber gepusht werden. Dazu subscriben die Views dem gewünschten Daten Stream. Verändern sich dann die Daten, werden die Änderungen über den Stream direkt an alle Subscriber gepusht ohne das der UI Thread geblockt wird.
Auf diese Art und Weise wird ein direkter Datenzugang verhindert und die Daten sind read-only. Die eigentliche Datenquelle wird ähnlich wie bei einer API angesprochen, die von den Views verwendet wird. Als zentrale Schnittstelle dient hier die eingangs beschriebene Fassade. Diese besteht aus Streams, die Daten liefern, wenn sich die Daten ändern und Methoden, um Änderungen an den Daten anzufordern oder bestimmte benutzerdefinierte Streams anzufordern.
Die eigentlichen Rohdaten sind erst verfügbar, nachdem sie durch den/die Stream(s) gepusht worden sind. Diese Abschirmung zentralisiert die gesamte Logik und zwingt die Views passiv auf die eingehenden Daten zu reagieren. Mit den Push basierten Diensten werden Angular View Komponenten hoch performant und nutzen dabei sowohl ChangeDetectionStrategy.OnPush, als auch die async Pipe für die gelieferten Stream Daten.
Daher ist das System lazy loading. Das UI-Framework braucht nicht nach Zustandsänderungen des View-Modells suchen und wartet stattdessen lazy darauf, dass ein neuer Zustand gepusht wird.
Praxisbeispiel
Als Beispiel für eine Push basierte Architektur dient eine einfache Anwendung, welche über den HTTP Request https://random-data-api.com/api/beer/random_beer?size=1 eine zufällige Bier Sorte erhält. Eine View zeigt die Biersorte an. Über ein kleines Input Feld kann die Anzahl der anzuzeigenden Biere variiert werden. Eine Fassade als zentrales Element pusht automatisiert die neuen Biersorten an die View, sobald diese verfügbar sind.
Als Vorbild für das Entwurfsmuster dient der Artikel von Thomas Burleson: https://thomasburlesonia.medium.com/push-based-architectures-with-rxjs-81b327d7c32d
Als erstes muss das State Management aufgesetzt werden:
export interface Beer { brand: string; name: string; style: string; hop: string; alcohol: string; } export interface BeerState { beerArray: Beer[]; size: number; }
Als nächstes initialisieren wir die Werte des Statemanagements der Fassade:
export interface BeerState { beerArray: Beer[]; size: number; } let _state: BeerState = { beerArray: [], size: 4 };
Die Fassade nutzt RxJS Streams um Daten direkt an die Views zu pushen. Diese Streams können auch automatisiert bei einer Änderung der States einen Rest API call ausführen:
combineLatest(this.size$) .pipe( //switchMap: Maps values to observable. Cancels the previous inner observable. switchMap(([size]) => { return this.findBeerArray(size); }) ) .subscribe((beerArray) => { this.updateState({ ..._state, beerArray }); });
Die Streams werden so aufgebaut, dass sie dauerhaft erhalten bleiben und nur bei einer Datenänderung aktiv werden:
export class BeerFacade { beerArray$ = this.state$.pipe( map((state) => state.beerArray), distinctUntilChanged() ); size$ = this.state$.pipe( map((state) => state.size), distinctUntilChanged() ); private updateState(state: BeerState) { this.store.next((_state = state)); } }
Für die bessere Handhabung werden die Streams zu einem Einzigen zusammengefasst:
vm$: Observable<BeerState> = combineLatest(this.beerArray$, this.size$).pipe( map(([beerArray, size]) => { return { beerArray, size }; }) );
Der HTTP Request kann in einen seperaten Data Service ausgelagert werden. Da es sich hierbei aber nur um ein kleines Beispielprogramm handelt, wird der Restcall innerhalb der Facade implementiert und ausgeführt:
/** RandomBeer REST call */ private findBeerArray(size: number): Observable<Beer[]> { const url = `https://random-data-api.com/api/beer/random_beer?size=${size}`; return this.http.get<Beer[]>(url); }
Die Fassade und der View Model Stream können nun einfach initialisiert und genutzt werden. Es ist unglaublich wenig Code notwendig:
export class AppComponent { vm$: Observable<BeerState> = this.facadeService.vm$; //facadeService is public for direct usage in html constructor(public facadeService: RandomBeerFacadeService) {} title = 'RandomBeerApp'; }
Die Daten des View Model Streams werden mit folgendem Codeschnippsel angezeigt:
<div *ngIf="vm$ | async as vm"> <ul> <li id="random-beer-list" *ngFor="let u of vm.beerArray"> Brand: {{ u.brand }}, Alcohol: {{ u.alcohol }}, Hop: {{ u.hop }} </li> </ul> <input id="input-rand-beer" type="number" (change)="facadeService.updateSize($event)" /> </div>
Dabei sorgt die Async Pipe von Angular dafür, dass immer die aktuellsten Daten angezeigt werden. Hier findet sich eine genaue Erklärung der Async Pipe.
Da der Fassaden Service als public deklariert wurde, werden die Inputs direkt an die updateSize Funktion der Fassade weitergegeben:
updateSize(selectedSize: any) { const size = selectedSize.target.value; this.updateState({ ..._state, size }); }
Als Resultat zeigt die Applikation zunächst 4 zufällige Biere an. Anschließend kann durch das Input Feld die Anzahl auf 7 erhöht werden. Dadurch wird die Funktion updateSize() aufgerufen, welche mittels updateState() den State des size$ streams aktualisiert. Als Folge dessen wird automatisiert ein neuer REST call ausgeführt und das Ergebnis an das View Model gepusht. Mit Hilfe der async Pipe werden somit die neusten Daten angezeigt. In unserem Fall die 7 zufälligen Biersorten.
Abschließend macht es Sinn die Funktionalität der einzelnen Fassaden Bestandteile zu testen:
it('should get individual Observable "stream" of vm data', (done) => { testFacade.vm$.subscribe((vm) => { expect(vm.size).toEqual(initStateMock.size); done(); }); }); it('should update state values', (done) => { const updatedStateMock: TestBeerState = { beerArray: [ { brand: 'Pabst Blue Ribbon', name: 'Two Hearted Ale', style: 'Merican Ale', hop: 'Sorachi Ace', alcohol: '2,9%', }, { brand: 'Bud Light', name: 'La fin Du Monde', style: 'Stout', hop: 'Bullion', alcohol: '2,7%', }, ], size: 2, }; testFacade['updateState'](updatedStateMock); testFacade.vm$.subscribe((vm) => { expect(vm).toEqual(updatedStateMock); done(); }); }); it('should update the size value', (done) => { const newSize = 9; const mockEvent = { target: { value: newSize, }, }; testFacade['updateSize'](mockEvent); testFacade.vm$.subscribe((vm) => { expect(vm.size).toEqual(newSize); done(); }); }); it('should perform a mocked http request', (done) => { const httpMock: HttpTestingController = TestBed.inject( HttpTestingController ); const mockResponse = { brand: 'Pabst Blue Ribbon', name: 'Two Hearted Ale', style: 'Merican Ale', hop: 'Sorachi Ace', alcohol: '2,9%', }; testFacade['findBeerArray'](1); testFacade.vm$.subscribe((tb) => { expect(tb.beerArray).toBeTruthy(); expect(tb.beerArray[0].brand).toBe(mockResponse.brand); expect(tb.beerArray[0].name).toBe(mockResponse.name); expect(tb.beerArray[0].style).toBe(mockResponse.style); expect(tb.beerArray[0].hop).toBe(mockResponse.hop); expect(tb.beerArray[0].alcohol).toBe(mockResponse.alcohol); done(); }); const mockRequest = httpMock.expectOne( 'https://random-data-api.com/api/beer/random_beer?size=1' ); mockRequest.flush(mockResponse); });
Ein Gedanke zu “Wie man eine Angular Fassade mit PUSH Architektur programmiert”