Typically, a PULL-based architecture is used in software development and only rarely a PUSH-based architecture. A service is called to request data from a server, which sends a request to the server and receives the required data. However, during this process, the main UI thread is blocked.
Angular itself regularly checks the view model for changes. If the view model changes due to a state change, Angular re-renders the view because Angular automatically adjusts the UI when the model changes. These continuous observations make the whole application slower.
Moreover, with the traditional pull approach, complexity increases. Especially when multiple views need the same data. What happens when the data changes? How are views notified that new data is available? Callback functions could help, but as a result, the application quickly becomes confusing. They also have a negative impact on handling and maintenance, especially for larger applications. After all, the objects have to be initialized individually and the dependencies or the order of the methods have to be considered.
A push-based architecture can be used to avoid this problem. Angular offers a great way to implement this with RxJS and the facades design pattern! The implementation is explained using an Angular example in this blog article and is transferable for all platforms and frameworks (Angular, React, Vue.js, etc.).
List of components
- IDE (Visual Studio Code)
- Angular CLI
What is a facade actually?
The facade design pattern is a central interface that communicates with various interfaces of one or more lower-level subsystems. It can perform additional functions both before and after a client request.
The facade object provides a unified and usually simplified interface to one or more subsystems. As an intermediary, it ensures that communication or access to the individual components of a subsystem is simplified, thereby also minimizing direct dependence on these components. It delegates the client calls in such a way that clients need to know neither the classes nor their relations and dependencies.
This is especially helpful in large systems when a subsystem contains many technically oriented classes that are rarely used externally. Using a facade reduces complexity by combining multiple interfaces into one. In addition, the subsystem can be extended more easily due to the loose coupling.
The PUSH Architecture
For a push based architecture, advanced design patterns like Redux or NgRx can be used. However, a very elegant and performant push based solution can also be achieved using RxJS.
Using the long-living RxJS Observable streams, changes in the data can be pushed to all subscribers. To do so, the views subscribe to the desired data stream. If the data changes, the changes are pushed directly to all subscribers via the stream without blocking the UI thread.
In this way, direct data access is prevented and the data is read-only. The actual data source is accessed similar to an API used by the views. The facade described at the beginning serves as the central interface here. This consists of streams that provide data when the data changes and methods to request changes to the data or request specific user-defined streams.
The actual raw data is only available after it has been pushed through the stream(s). This shielding centralizes all logic and forces views to passively respond to incoming data. With the push based services, Angular View components become highly performant using both ChangeDetectionStrategy.OnPush, and the async pipe for the delivered stream data.
Therefore, the system is lazy loading. The UI framework does not need to check for state changes of the view model and instead lazily waits for a new state to be pushed.
Practical example
As an example for a push based architecture a simple application is used, which receives a random beer type via the HTTP request https://random-data-api.com/api/beer/random_beer?size=1. The type of beer is displayed in a view. An input field can be used to vary the number of beers to be shown. A facade as a central element automatically pushes the new beers to the view as soon as they are available.
As a basis for the design pattern serves the article by Thomas Burleson: https://thomasburlesonia.medium.com/push-based-architectures-with-rxjs-81b327d7c32d
The first step is to set up the state management:
export interface Beer { brand: string; name: string; style: string; hop: string; alcohol: string; } export interface BeerState { beerArray: Beer[]; size: number; }
Next, we initialize the values of the facade’s state management:
export interface BeerState { beerArray: Beer[]; size: number; } let _state: BeerState = { beerArray: [], size: 4 };
The facade uses RxJS streams to push data directly to the views. These streams can also automatically execute a Rest API call when the states change:
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 }); });
Streams are set up to be persistent and only become active when data changes:
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)); } }
For better management, the streams are combined into a single one:
vm$: Observable<BeerState> = combineLatest(this.beerArray$, this.size$).pipe( map(([beerArray, size]) => { return { beerArray, size }; }) );
The HTTP request can be outsourced to a separate data service. However, since this is only a small sample program, the Rest API call is implemented and executed within the Facade:
/** 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); }
The facade and view model stream can now be easily initialized and used. Incredibly less code is required:
export class AppComponent { vm$: Observable<BeerState> = this.facadeService.vm$; //facadeService is public for direct usage in html constructor(public facadeService: RandomBeerFacadeService) {} title = 'RandomBeerApp'; }
The view model stream data is displayed with the following code snippet:
<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>
Angular’s Async Pipe ensures that the most current data is always displayed. Here is a detailed explanation of the async pipe.
Since the facade service is declared as public, the inputs are passed directly to the updateSize function of the facade:
updateSize(selectedSize: any) { const size = selectedSize.target.value; this.updateState({ ..._state, size }); }
As a result, the application initially displays 4 random beers. Afterwards the number can be increased to 7 by the input field. This calls the updateSize() function, which updates the state of the size$ stream using updateState(). As a result, a new REST call is automatically executed and the result is pushed to the view model. With the help of the async pipe the latest data is displayed. In our case the 7 random beer types.
Finally, it makes sense to test the functionality of the individual facade components:
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); });
One thought on “How to build a PUSH based architecture in Angular – Facade Design Pattern”