Zum Hauptinhalt springen

Anti-Pattern-Katalog: Imperatives Angular

Einordnung

Angular ist ein:

  • deklaratives UI-Framework
  • mit reaktivem State-Modell
  • basierend auf RxJS / Signals
  • mit Template-getriebener Rendering-Logik

Viele Projekte entwickeln sich jedoch zu einem imperativ-objektorientierten Mischsystem, das Angular nur noch syntaktisch nutzt.

Dieses Dokument beschreibt typische Anti-Pattern, ihre Ursachen und bessere Alternativen.


1. Sofortiges Subscriben in der Komponente

❌ Anti-Pattern

ngOnInit() {
this.http.get<User[]>('/api/users')
.subscribe(users => {
this.users = users;
});
}

Problem

  • Reaktive Pipeline wird beendet
  • Datenfluss wird in imperativen Zustand übersetzt
  • Subscription-Management notwendig
  • Testbarkeit sinkt
  • Stream-Komposition unmöglich

✅ Besser

users$ = this.http.get<User[]>('/api/users');

Template:

<li *ngFor="let user of users$ | async">
{{ user.name }}
</li>

Oder mit Signals:

users = toSignal(this.http.get<User[]>('/api/users'));

2. Observable → Promise Konvertierung

❌ Anti-Pattern

async load() {
const users = await firstValueFrom(
this.http.get<User[]>('/api/users')
);
this.users = users;
}

Problem

  • Verlust von Cancellation
  • Verlust von Stream-Komposition
  • Zeitdimension wird eliminiert
  • Reaktivität wird zerstört

Wann vertretbar?

Nur wenn:

  • einmaliger Request
  • kein weiterer Stream-Kontext
  • explizite Imperativ-Schnittstelle benötigt

3. Selbstgeschriebenes State Management

❌ Anti-Pattern

class UserStore {
private users: User[] = [];

setUsers(users: User[]) {
this.users = users;
}

getUsers() {
return this.users;
}
}

Problem

  • Kein reaktiver Output
  • Keine Ableitungen
  • Keine Zeitdimension
  • Mutation statt Transformation

✅ Besser

private usersSubject = new BehaviorSubject<User[]>([]);

users$ = this.usersSubject.asObservable();

setUsers(users: User[]) {
this.usersSubject.next(users);
}

Oder Signals:

users = signal<User[]>([]);

Oder dediziertes State-System (NgRx, ComponentStore etc.).


4. Doppelte Zustände

❌ Anti-Pattern

users$: Observable<User[]>;

users: User[];

ngOnInit() {
this.users$.subscribe(u => this.users = u);
}

Problem

  • Zwei Wahrheiten
  • Race Conditions möglich
  • Debugging erschwert
  • Inkonsistente UI

Grundregel

Keine Spiegelung von Streams in lokale Felder.


5. Lifecycle-Überladung

❌ Anti-Pattern

ngOnInit() {
this.load();
this.initForm();
this.setupListeners();
this.registerSomething();
}

Problem

  • Imperative Initialisierungs-Orchestrierung
  • Versteckte Seiteneffekte
  • Schwer testbar
  • Komplexität steigt linear

✅ Besser

Deklarative Initialisierung:

form = new FormGroup(...);

data$ = this.http.get(...);

vm$ = combineLatest([this.data$, this.other$])
.pipe(map(([data, other]) => ({ data, other })));

6. Manuelle Subscription-Verwaltung

❌ Anti-Pattern

subscription: Subscription;

ngOnInit() {
this.subscription = this.some$.subscribe();
}

ngOnDestroy() {
this.subscription.unsubscribe();
}

Problem

  • Boilerplate
  • Memory-Leak-Risiko
  • Imperatives Lifecycle-Management

✅ Besser

  • async Pipe
  • takeUntilDestroyed()
  • Signals
  • Higher-order Streams

7. Getter-Logik im Template

❌ Anti-Pattern

get filteredUsers() {
return this.users.filter(u => u.active);
}

Template:

<li *ngFor="let user of filteredUsers">

Problem

  • Getter wird bei jeder Change Detection ausgeführt
  • Performance-Probleme
  • Nicht transparent

✅ Besser

filteredUsers$ = this.users$
.pipe(map(users => users.filter(u => u.active)));

8. Validierung als imperatives Kontrollsystem

❌ Anti-Pattern

if (!this.form.valid) {
this.error = "Invalid form";
return;
}

Oder komplexe if-Kaskaden in submit().

Problem

  • Imperative Kontrolllogik
  • Validierung ist kein Datenfluss
  • Wiederverwendbarkeit gering

✅ Besser

  • Reactive Forms mit Validatoren
  • Ableitung von form.statusChanges
  • Declarative Error Rendering

9. Business-Logik in der Komponente

❌ Anti-Pattern

Komponente enthält:

  • API-Calls
  • State-Management
  • Validierung
  • Mapping
  • Domänenlogik

Problem

Komponente wird:

  • untestbar
  • instabil
  • überverantwortlich

Architekturregel

Komponente = Projektionsebene.

Nicht:

  • Orchestrator
  • Domain-Service
  • State-Maschine

10. Mutative Array-Operationen

❌ Anti-Pattern

this.users.push(newUser);

Problem

  • Keine neue Referenz
  • Change Detection Probleme
  • Seiteneffekte

✅ Besser

this.users = [...this.users, newUser];

Oder:

this.usersSubject.next([...current, newUser]);

11. Manuelles DOM-Manipulieren

❌ Anti-Pattern

document.querySelector(...);

Problem

  • Bruch des deklarativen Paradigmas
  • Rendering-Engine wird umgangen
  • Testbarkeit sinkt

12. Services als globale Zustandscontainer

❌ Anti-Pattern

@Injectable({ providedIn: 'root' })
export class GlobalStateService {
currentUser: User;
}

Problem

  • Unsichtbare Abhängigkeiten
  • Seiteneffekte
  • Test-Hölle

13. „Alles ist eine Klasse“

❌ Anti-Pattern

  • UI-Models als Klassen
  • DTOs als Klassen mit Methoden
  • Mapper als Klassen
  • Hilfsfunktionen als Klassen

Problem

Unnötige Objekt-Identität in einer datenflussbasierten Umgebung.


14. Imperatives Error Handling

❌ Anti-Pattern

try {
await ...
} catch (e) {
this.error = e;
}

In einem eigentlich reaktiven Kontext.

✅ Besser

data$ = this.http.get(...)
.pipe(
catchError(err => of({ error: true }))
);

Meta-Problem

Imperatives Angular führt zu:

  • Datenflussbruch
  • Doppeltem State
  • versteckten Seiteneffekten
  • Lifecycle-Missbrauch
  • unnötiger Komplexität
  • kognitiver Inkonsistenz

Kernaussage

Angular ist kein objektorientiertes UI-System.

Es ist:

  • deklarativ
  • reaktiv
  • datenflussgetrieben

Wer versucht, es imperativ zu kontrollieren, arbeitet gegen das Framework.


Architekturprinzip

UI ist eine Projektion von State.

Nicht ein Objekt mit internen Geheimnissen.