Dieser Artikel behandelt das Fassaden Pattern in NgRx. Der Autor setzt voraus, dass ein entsprechendes Grundwissen im Umgang mit NgRx vorhanden ist.
Die Fassade entkoppelt NgRx von unserer Anwendung. Dabei werden Selektoren als Properties vom Typ Observable angeboten. Die Actions werden zu normalen Methoden.
Eine Videoversion gibt es hier: https://youtu.be/K4dpVXuhm14
Theorie
Die Fassade ist ein Architekturpattern und setzt eine Anwendung voraus, welche bereits aus mehreren Modulen besteht. Dabei sollen diese Module weitestgehend voneinander unabhängig sein.
Die Architektur, welche hier herangezogen wird, besteht aus zwei Ebenen. In der ersten befinden sich die einzelnen Domänen.
Die sogenannte App-Shell verwaltet diese, indem es die Domänen über die Routenkonfiguration zur Verfügung stellt. Die App-Shell stellt auch die zentralen Services bereit.
Daneben gibt es noch die sogennanten Shared Module, welche den Domänen zur Verwendung bereitstehen.
Die zweite Ebene besteht aus den Submodulen, welche sich in einer Domäne befinden. Dabei unterscheiden wir zwischen vier Modultypen.
Die sogenannten Containerkomponenten befinden sich im Modultyp feature. Die UI-Komponenten im gleichnamigen ui Typ und der NgRx Feature State in data. Daneben gibt es noch den model Typen, wo sich ausschließlich die Domaintypen befinden.
Um die Fassade effektiv einsetzen zu können, muss eine Unterstützung für die sognannte Module Encapsulation und Abhängigkeitsregeln vorhanden sein.
So soll man bei der Module Encapsulation entscheiden können, welche Elemente eines Modules von außen erreichbar sind. Bei den Abhänigkeitsregeln kann man festlegen, welche Module überhaupt aufeinander zugreifen können.
So soll der Typ feature auf alle anderen Typen zugreifen können. data und ui jedoch nur auf model.
Für detailliertere Informationen bieten sich folgende Links an:
NgRx Best Practices Series: 2. Modularity — Rainer HahnekampEnforce Module Boundaries | NxGitHub — softarc-consulting/sheriff: Lightweight Modularity for TypeScript applications
Ein zentrales Element der Fassade ist die Module Encapsulation. Dadurch können wir gewährleisten, dass die Container nur die Fassade und nicht NgRx direkt verwenden.
Implementierung
Gehen wir von einem klassischen CRUD Anwendungsfall für eine Entiät Customer aus. Wir benötigen Funktionen zur Listendarstellung, Hinzufügen, Ändern sowie Löschen.
Somit hätten wir folgenden State:
export interface State {
customers: Customer[];
}
Und NgRx Actions:
export const customersActions = createActionGroup({
source: ‘Customers’,
events: {
load: emptyProps(),
// further CRUD-based actions
},
});
Schlußendlich brauchen wir zwei Selektoren, wobei NgRx den ersten bereits über createFeature bereitstellt und der zweite manuell erstellt wird.
const selectAll = customersFeature.selectCustomers;
const selectById = (id: number) => createSelector(
selectAll,
(state: Customer[]) =>
state.find((p) => p.id === id)
);
export const fromCustomers = {selectAll, selectById};
Ohne einer Fassade müssten die Komponenten direkt mit dem Store Service arbeiten. Dabei werden die bekannten Methoden dispatch sowie select angewendet.
export class EditCustomerComponent implements OnInit {
#store = inject(Store);
protected customers$: Observable<Customer[]> | undefined;
ngOnInit() {
this.customers$ = this.#store
.select(fromCustomers.selectAll());
}
submit(customer: Customer) {
this.#store.dispatch(customersActions.update({ customer }));
}
}
Wenden wir uns nun der Fassade zu. Sie stellt nach außen für die Actions und parametrierbar Selektoren Methoden bereit. Normale Selektoren zeigt sie als Properties vom Typ Observable an.
@Injectable({ providedIn: ‘root’ })
export class CustomersFacade {
#store = inject(Store);
get customers$(): Observable<Customer[]> {
return this.#store.select(fromCustomers.selectAll);
}
update(customer: Customer) {
this.#store.dispatch(customersActions.update({ customer }));
}
}
Aus der Perspektive der Komponte ergeben sich folgende Änderungen:
export class EditCustomerComponent implements OnInit {
#facade = inject(CustomersFacade);
protected customer$: Observable<Customer> | undefined;
ngOnInit() {
this.customer$ = this.#facade.customers$;
}
update(customer: Customer) {
this.#facade.update(customer);
}
}
Durch den Einsatz der CustomersFacade weiß die EditCustomerComponent gar nicht mehr, dass hier überhaupt NgRx im Einsatz ist. Es sind normale Methodenaufrufe eines gewöhnlichen Angular Services.
Natürlich müssen wir nun auch alle anderen Komponenten dementsprechend anpassen.
Vorteile
Entkopplung
Was genau bringt nun diese Entkopplung von NgRx? Gibt es überhaupt Vorteile oder ist es nur simples Overengineering?
Der Hauptvorteil ist der minimale Aufwand bei einem möglichen Wechsel von NgRx.
Viele meiner Kollegen, deren Meinung ich sehr schätze, argumentieren, dass ein Austausch von NgRx sehr unwahrscheinlich ist. NgRx ist ein kritisches Element einer Anwendung und man könnte genausogut sagen, dass man Angular abstrahiert, weil man vielleicht irgendwann React einsetzen möchte.
Ein weiteres Gegenargument sind mögliche Anti-Patterns. So kann zum Beispiel eine Methode in einer Fassade nicht nur eine Action dispatchen sondern auch gleich ein Observable von einem Selektor zurückgeben.
Bezüglich der Anti-Patterns muss erwidert werden, dass jede Maßnahme missbraucht werden kann. Das ist allerdings noch lange kein Grund diese nicht anzuwenden. Vielmehr muss ein internes Qualitätsmanagement garantieren, dass derartiger Code entdeckt und nicht gemergt wird.
Wie steht es allerdings mit dem Argument, dass man NgRx nicht austauschen wird?
Tatsächlich gibt es eine Reihe von nicht ganz unwahrscheinlich Szenarien, wo genau dies erforderlich ist:
Geänderte Anforderungen: Im Verlauf einer Anwendung können sich gewisse Rahmenbedingungs ändern. Beispielsweise könnten Teile einer Logik vom Frontend ins Backend wandern oder aus einem komplizierten Fall wird ein klassischer CRUD Anwendungsfall.
Wenn somit die einzige Aufgabe von NgRx besteht, den Proxy zum Backend zu stelen, kann man es durch ein einfaches Service ersetzen.
Durch die Fassade ist dieser Umbau sehr einfach möglich. Sie ruft anstatt der Action den HttpClient direkt auf. Zu den Komponenten hin, ändert sich gar nichts. Für sie sind es nach wie vor nur Methoden und Properities.Vorbereitet für mehr: Vor allem Teams, welche noch nicht so viel Erfahrung mit NgRx haben, können von dessen Funktionsumfang eingeschüchtert sein. Als Folge davon entscheiden sie sich häufig mit einem BehaviorSubject zu starten, welches in einem Service gekapselt ist.
Grundsätzlich ist das ein guter Ansatz. Allerdings besteht das Risiko den Zeitpunkt auf NgRx zu wechseln, zu versäumen. Man ist dann leider nicht nur Autor der Applikation sondern auch von einer eigenen State Management Library. Das ist definitiv nicht das Ziel.
Auch hier verschafft die Fassade durch ihre Abstraktion Vorteile. Nach außen sehen nämlich Services mit einem BehaviorSubject genauso aus, wie die Fassade. Nun haben wir keine Probleme mehr den Zeitpunkt zu verpassen und können migrieren, wann immer wir möchten.Overengineering Reversed: Teams im Anfangsstadium kann aber auch das Gegenteil zustoßen. Sie können ein wenig mit NgRx übertreiben. Auch hier stellt die Fassade optimale Ausgangsbedingungen dar, um wieder auf das BehaviorSubject Pattern zurückgehen zu können.Component und Signal Store: NgRx hat sich mit dem Component und dem kommenden Signal Store Konkurrenz aus dem eigenen Haus geschaffen. Nachdem man hier nach wie vor im “NgRx Universum” bleibt, ist ein Wechsel zu eines der beiden Alternativen durchaus attraktiv.
Eingebaute Logik
Gewisse Anwendungsfälle sind nur schwer mit NgRx umsetzbar.
Ein Beispiel wäre ein “Loading On-Demand”. Der Laderequest für die Customers soll erst beim ersten Aufruf des Selektors stattfinden.
Ein Selektor kann allerdings keine Seiteneffekte ausführen und damit auch keine Action dispatchen.
Nachdem die Fassade den Selektor über eine getter Methode bereitstellt, hat sie auch die Möglichkeit Seiteneffekte auszuführen. Sie trackt den Ladezustand und dispatcht beim erstmaligen Aufruf der Methode die Action:
export class CustomersFacade {
#isLoaded = false;
#store = inject(Store);
get customers$(): Observable<Customer[]> {
this.#assertLoaded();
return this.#store.select(fromCustomers.selectAll);
}
byId(id: number): Observable<Customer | undefined> {
this.#assertLoaded();
return this.#store.select(fromCustomers.selectById(id));
}
#assertLoaded() {
if (!this.#isLoaded) {
this.#store.dispatch(customersActions.load());
this.#isLoaded = true;
}
}
}
Wie bereits weiter oben erwähnt, müssen wir uns beim Einbauen etwas zurückhalten. Wir sollten nicht die Fassade als Möglichkeit ansehen, grundlegende Prinzipien von NgRx umzubauen. Zum Beispiel sollte ein Methode nicht eine Action dispatchen und gleichzeitig ein Observable zurückliefern.
Einfachere Komponententests
Wenn wir Komponenten testen, welche NgRx verwenden, dann gibt es eine Reihe von nützlichen Testmethoden, die uns NgRx anbietet.
Die Hauptmethode ist sicherlich provideMockStore, welches uns einen kompletten gemockten Store zur Verfügung stellt:
const sabine: Customer = {
id: 1,
firstname: ‘Sabine’,
name: ‘Miscovics’,
country: ‘AT’,
birthdate: ‘1993-05-09’
};
it(‘should show customers’, () => {
const fixture = TestBed.configureTestingModule({
imports: [CustomersContainerComponent],
providers: [
provideRouter([]),
provideMockStore({
initialState: {
customers: {
customers: [ sabine ],
},
currentPage: 1,
pageCount: 1,
},
}),
],
}).createComponent(CustomersContainerComponent);
fixture.detectChanges();
expect(
document
.querySelector(‘[data-testid=row-customer] p.name’)
?.innerHTML.trim()
).toBe(‘Sabine Miscovics’);
});
Obwohl wir uns nicht mehr eigene Mocks schreiben müssen, ist die Situation relativ ungünstig.
So müssen wir einerseits genau über die Struktur des Featurestates Bescheid wissen. Darüber hinaus müssen wir auch den Featurekey kennen.
Aus der Sichtweise der Komponente sind dies Informationen, die sie eigentlich nicht wirklich betreffen. Die Komponente möchte einfach aus dem Store Daten beziehen und nicht den Store selber testen.
Auch hier hat die Fassade mit wenig Aufwand eine große Wirkung.
it(‘should show customers’, () => {
const facadeMock: Partial<CustomersFacade> = {
get customers$(): Observable<Customer[]> {
return of([ sabine ]);
},
};
const fixture = TestBed.configureTestingModule({
imports: [CustomersContainerComponent],
providers: [
provideRouter([]),
{ provide: CustomersFacade, useValue: facadeMock },
],
}).createComponent(CustomersContainerComponent);
// rest of the test
});
Für den Test stellt sich NgRx nun als generisches Service vor, dass gemockt werden kann, wie auch alle anderen Services in Angular.
Wir müssen weder über den State, noch über den Featurekey Bescheid wissen.
Zusammenfassung
Die Fassade ist ein Pattern, welches sehr einfach zu implementieren ist und viele Vorteile mit sich bringt.
Es abstrahiert die Actions, indem es sie als normale Methoden nach außen darstellt. Bei den Selektoren sind es Properties vom Typ Observable.
Aus der Architektursicht bekommen wir eine Entkopplung. Wir können jedoch zusätzliche Logik einbauen und haben auch einfachere Komponententests.
Die Fassade mag am Anfang wie Overengineering wirken, ist es aber nicht. Das Kosten- / Nutzenverhältnis spricht eindeutig für ihren Einsatz. Am Besten einfach selber ausprobieren!
Fassade Pattern in NgRx was originally published in ngconf on Medium, where people are continuing the conversation by highlighting and responding to this story.