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
asyncPipetakeUntilDestroyed()- 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.