Angular & Facades: So entkoppelst du deine Components und machst deinen Code wartbarer

Sobald eine Angular-Anwendung wächst kann es schnell unübersichtlich werden. Komponenten die beispielsweise direkt auf Backend-APIs zugreifen oder den Application-State manipulieren sind hier aus meiner Erfahrung heraus keine Seltenheit und führen langfristig zu Problemen. Komponenten werden zu groß, die Testbarkeit leidet und das Austauschen von Stores oder APIs wird mühsam und kostspielig. Ein Lösungsansatz für diese Problematik könnte die Verwendung von Facades sein. Facades sind eine Art von Design-Pattern, das eine vereinfachte Schnittstelle zu einer komplexen API oder einem System bereitstellt. In Angular-Anwendungen können Facades verwendet werden, um den Zugriff auf verschiedene Teile der Anwendung zu abstrahieren und zu vereinheitlichen. Sie fungieren als Vermittler zwischen der UI und den Services, die die Logik und den Zustand der Anwendung verwalten. Was sind Facades (in Angular)? Stell dir eine Fernbedienung für ein komplexes Soundsystem vor. Anstatt dich mit den unzähligen Knöpfen und Einstellungen der einzelnen Komponenten (Verstärker, Equalizer, Lautsprecher) auseinandersetzen zu müssen, bietet dir die Fernbedienung eine einfache Möglichkeit, die Lautstärke zu regeln, die Quelle auszuwählen oder das System ein- und auszuschalten. Genau diese Rolle übernimmt eine Facade in Angular. Eine Angular Facade ist ein Service, der als vereinfachte Schnittstelle zu einem komplexeren Subsystem dient. Dieses Subsystem kann Backend-APIs, State-Management-Lösungen oder andere komplexe Dienste umfassen. Die Facade kapselt die zugrunde liegende Komplexität und bietet den Komponenten eine intuitive und leicht verständliche API. Ein einfaches Beispiel: Angenommen, du möchtest Benutzerdaten abrufen. Ohne Facade könnte deine Komponente direkt den HttpClient und einen spezifischen API-Service nutzen. Mit einer Facade sieht das Ganze so aus: // The actual API service @Injectable({ providedIn: 'root', }) export class UserService { readonly #httpClient = inject(HttpClient); getUsers() { return this.#httpClient.get( 'https://jsonplaceholder.typicode.com/users' ); } } // The facade to abstract the Access to the API @Injectable({ providedIn: 'root', }) export class UserDataFacade { readonly #userService = inject(UserService); getUsers(): Observable { return this.#userService.getUsers(); } } // The component that uses the facade @Component({ selector: 'app-user-profile', imports: [AsyncPipe], template: `@for (user of users$ | async; track user.id) { {{ user.name }} Email: {{ user.email }} Phone: {{ user.phone }} Website: {{ user.website }} }`, changeDetection: ChangeDetectionStrategy.OnPush, }) export class UserListComponent { readonly #userDataFacade = inject(UserDataFacade); protected readonly users$ = this.#userDataFacade.getUsers(); } In dem hier zu sehenden Beispiel delegiert die UserDataFacade den Zugriff auf den UserService und stellt eine vereinfachte API bereit, die von der Komponente verwendet werden kann. Dadurch wird die Komplexität des zugrunde liegenden Systems verborgen und die Testbarkeit sowie Wartbarkeit der Anwendung verbessert. Angenommen die User-Daten würden durch 50 andere Komponenten verwendet, dann würde es genügen die Facade zu ändern und nicht alle 50 Komponenten. Das ist einer der großen Vorteile von Facades. Die Tests für die Facade sind ebenfalls einfacher, da du nur die Facade selbst testen musst, anstatt alle Komponenten, die den Service verwenden. Du kannst Mock-Implementierungen der Facade verwenden, um sicherzustellen, dass die Komponenten korrekt mit der Facade interagieren, was folgendes Beispiel zeigt: describe('UserListComponent', () => { let component: UserListComponent; let fixture: ComponentFixture; let mockUserFacade: Partial; beforeEach(async () => { mockUserFacade = { getUsers: jest.fn().mockRejectedValue( of([ { id: 1, name: 'Leanne Graham', username: 'Bret', email: 'Sincere@april.biz', address: { street: 'Kulas Light', suite: 'Apt. 556', city: 'Gwenborough', zipcode: '92998-3874', geo: { lat: '-37.3159', lng: '81.1496', }, }, phone: '1-770-736-8031 x56442', website: 'hildegard.org', company: { name: 'Romaguera-Crona', catchPhrase: 'Multi-layered client-server neural-net', bs: 'harness real-time e-markets', }, }, ]) ), }; await TestBed.configureTestingModule({ imports: [UserListComponent], providers: [{ provide: UserDataFacade, useValue: mockUserFacade }], }).compileComponents(); fixture = TestBed.createComponent(UserListComponent); component = fixture.componentInstance; fixture.detectCha

May 8, 2025 - 19:29
 0
Angular & Facades: So entkoppelst du deine Components und machst deinen Code wartbarer

Sobald eine Angular-Anwendung wächst kann es schnell unübersichtlich werden. Komponenten die beispielsweise direkt auf Backend-APIs zugreifen oder den Application-State manipulieren sind hier aus meiner Erfahrung heraus keine Seltenheit und führen langfristig zu Problemen. Komponenten werden zu groß, die Testbarkeit leidet und das Austauschen von Stores oder APIs wird mühsam und kostspielig.

Ein Lösungsansatz für diese Problematik könnte die Verwendung von Facades sein. Facades sind eine Art von Design-Pattern, das eine vereinfachte Schnittstelle zu einer komplexen API oder einem System bereitstellt. In Angular-Anwendungen können Facades verwendet werden, um den Zugriff auf verschiedene Teile der Anwendung zu abstrahieren und zu vereinheitlichen. Sie fungieren als Vermittler zwischen der UI und den Services, die die Logik und den Zustand der Anwendung verwalten.

Was sind Facades (in Angular)?

Stell dir eine Fernbedienung für ein komplexes Soundsystem vor. Anstatt dich mit den unzähligen Knöpfen und Einstellungen der einzelnen Komponenten (Verstärker, Equalizer, Lautsprecher) auseinandersetzen zu müssen, bietet dir die Fernbedienung eine einfache Möglichkeit, die Lautstärke zu regeln, die Quelle auszuwählen oder das System ein- und auszuschalten. Genau diese Rolle übernimmt eine Facade in Angular.

Eine Angular Facade ist ein Service, der als vereinfachte Schnittstelle zu einem komplexeren Subsystem dient. Dieses Subsystem kann Backend-APIs, State-Management-Lösungen oder andere komplexe Dienste umfassen. Die Facade kapselt die zugrunde liegende Komplexität und bietet den Komponenten eine intuitive und leicht verständliche API.

Ein einfaches Beispiel: Angenommen, du möchtest Benutzerdaten abrufen. Ohne Facade könnte deine Komponente direkt den HttpClient und einen spezifischen API-Service nutzen. Mit einer Facade sieht das Ganze so aus:


// The actual API service
@Injectable({
  providedIn: 'root',
})
export class UserService {
  readonly #httpClient = inject(HttpClient);

  getUsers() {
    return this.#httpClient.get<User[]>(
      'https://jsonplaceholder.typicode.com/users'
    );
  }
}

// The facade to abstract the Access to the API
@Injectable({
  providedIn: 'root',
})
export class UserDataFacade {
  readonly #userService = inject(UserService);

  getUsers(): Observable<User[]> {
    return this.#userService.getUsers();
  }
}

// The component that uses the facade
@Component({
  selector: 'app-user-profile',
  imports: [AsyncPipe],
  template: `@for (user of users$ | async; track user.id) {
    

{{ user.name }}

Email: {{ user.email }}

Phone: {{ user.phone }}

Website: {{ user.website }}

}`
, changeDetection: ChangeDetectionStrategy.OnPush, }) export class UserListComponent { readonly #userDataFacade = inject(UserDataFacade); protected readonly users$ = this.#userDataFacade.getUsers(); }

In dem hier zu sehenden Beispiel delegiert die UserDataFacade den Zugriff auf den UserService und stellt eine vereinfachte API bereit, die von der Komponente verwendet werden kann. Dadurch wird die Komplexität des zugrunde liegenden Systems verborgen und die Testbarkeit sowie Wartbarkeit der Anwendung verbessert. Angenommen die User-Daten würden durch 50 andere Komponenten verwendet, dann würde es genügen die Facade zu ändern und nicht alle 50 Komponenten. Das ist einer der großen Vorteile von Facades.

Die Tests für die Facade sind ebenfalls einfacher, da du nur die Facade selbst testen musst, anstatt alle Komponenten, die den Service verwenden. Du kannst Mock-Implementierungen der Facade verwenden, um sicherzustellen, dass die Komponenten korrekt mit der Facade interagieren, was folgendes Beispiel zeigt:

describe('UserListComponent', () => {
  let component: UserListComponent;
  let fixture: ComponentFixture<UserListComponent>;

  let mockUserFacade: Partial<UserDataFacade>;

  beforeEach(async () => {
    mockUserFacade = {
      getUsers: jest.fn().mockRejectedValue(
        of([
          {
            id: 1,
            name: 'Leanne Graham',
            username: 'Bret',
            email: 'Sincere@april.biz',
            address: {
              street: 'Kulas Light',
              suite: 'Apt. 556',
              city: 'Gwenborough',
              zipcode: '92998-3874',
              geo: {
                lat: '-37.3159',
                lng: '81.1496',
              },
            },
            phone: '1-770-736-8031 x56442',
            website: 'hildegard.org',
            company: {
              name: 'Romaguera-Crona',
              catchPhrase: 'Multi-layered client-server neural-net',
              bs: 'harness real-time e-markets',
            },
          },
        ])
      ),
    };

    await TestBed.configureTestingModule({
      imports: [UserListComponent],
      providers: [{ provide: UserDataFacade, useValue: mockUserFacade }],
    }).compileComponents();

    fixture = TestBed.createComponent(UserListComponent);
    component = fixture.componentInstance;

    fixture.detectChanges();
    await fixture.whenStable();
  });

  it('should create', () => {
    expect(component).toBeTruthy();
  });

  it('should call getUsers on init', () => {
    expect(mockUserFacade.getUsers).toHaveBeenCalledTimes(1);
  });

  // ... more tests
});

Best Practices bei der Verwendung von Facades

  • Granularität: Halte die Facade granular und spezifisch für einen bestimmten Anwendungsfall oder eine bestimmte Funktionalität. Vermeide es, eine große Facade zu erstellen, die viele verschiedene Funktionen abdeckt. Genauso ist es nicht sinnvoll eine Facade für jede Komponente zu erstellen. Eine Facade sollte immer mehrere Komponenten bedienen können. Die Balance zwischen Granularität und Wiederverwendbarkeit ist hier entscheidend.
  • Verwende Dependency Injection: Nutze Angulars Dependency Injection, um Abhängigkeiten in der Facade zu verwalten. Dadurch wird die Testbarkeit und Wartbarkeit verbessert.
  • Vermeide direkte Abhängigkeiten: Vermeide direkte Abhängigkeiten zwischen der Facade und den Komponenten. Verwende stattdessen Observable oder andere Mechanismen, um die Kommunikation zu ermöglichen.
  • Vermeide zu viel Logik: Halte die Logik in der Facade minimal. Die Facade sollte hauptsächlich als Vermittler fungieren und nicht zu viel Geschäftslogik enthalten. Diese sollte in den genutzten Services liegen.

Vorteile von Facades

  • Abstraktion: Facades abstrahieren die Komplexität des zugrunde liegenden Systems und bieten eine vereinfachte API.
  • Testbarkeit: Facades erleichtern das Testen von Komponenten, da sie eine klare Schnittstelle bieten und die zugrunde liegende Implementierung verbergen.
  • Wartbarkeit: Änderungen am zugrunde liegenden System erfordern keine Änderungen an den Komponenten, die die Facade verwenden.
  • Konsistenz: Facades fördern eine konsistente API für den Zugriff auf verschiedene Teile der Anwendung, was die Lesbarkeit und Wartbarkeit des Codes verbessert.
  • Flexibilität: Facades ermöglichen es, verschiedene Implementierungen oder Strategien auszutauschen, ohne dass die Komponenten davon betroffen sind.
  • Entkopplung: Facades entkoppeln die Komponenten von den spezifischen Implementierungen der Dienste, was die Modularität und Wiederverwendbarkeit des Codes erhöht.
  • Zentralisierung: Facades bieten einen zentralen Ort für die Verwaltung von Abhängigkeiten und Konfigurationen, was die Übersichtlichkeit und Wartbarkeit des Codes verbessert.
  • Erweiterbarkeit: Facades ermöglichen es, neue Funktionen oder Dienste hinzuzufügen, ohne dass die bestehenden Komponenten geändert werden müssen.
  • Fehlerbehandlung: Facades können eine zentrale Stelle für die Fehlerbehandlung bieten, was die Konsistenz und Wartbarkeit des Codes verbessert.
  • Performance: Facades können Optimierungen wie Caching oder Lazy Loading implementieren, um die Leistung der Anwendung zu verbessern.
  • Dokumentation: Facades bieten eine klare und konsistente API, die die Dokumentation und das Verständnis der Anwendung erleichtert.
  • Best Practices: Facades fördern bewährte Praktiken wie Separation of Concerns und Single Responsibility Principle, was zu einem saubereren und wartbareren Code führt.

Nachteile von Facades

  • Zusätzliche Komplexität: Facades können zusätzliche Komplexität in die Anwendung einführen, insbesondere wenn sie nicht gut strukturiert sind.
  • Überabstraktion: Facades können zu einer Überabstraktion führen, wenn sie zu viele Funktionen oder Abstraktionen bieten, die nicht benötigt werden.
  • Leistungsprobleme: Facades können in einigen Fällen die Leistung beeinträchtigen, insbesondere wenn sie nicht effizient implementiert sind.
  • Wartungsaufwand: Facades erfordern zusätzlichen Wartungsaufwand, insbesondere wenn sich die zugrunde liegenden Systeme ändern.
  • Testaufwand: Facades erfordern zusätzliche Tests, um sicherzustellen, dass sie korrekt funktionieren und die zugrunde liegenden Systeme korrekt abstrahieren.

Fazit

Angular Facades sind ein mächtiges Werkzeug, um die Architektur deiner Anwendung zu verbessern. Durch die Entkopplung von Komponenten von komplexen Abhängigkeiten wie Backend-APIs und State-Management-Lösungen schaffst du eine sauberere, wartbarere und testbarere Codebasis. Wenn du in größeren Angular-Projekten arbeitest, solltest du den Einsatz von Facades unbedingt in Betracht ziehen, um langfristig die Qualität und Flexibilität deiner Anwendung zu gewährleisten.

Lass mich gerne wissen, wie du Facades in deinen Projekten einsetzt oder ob du noch weitere Fragen zu diesem Thema hast. Ich freue mich auf den Austausch mit dir!