46 Design Patterns mit TypeScript

Design Patterns sind wiederverwendbare Lösungen für häufig auftretende Probleme in der Softwarearchitektur. Sie sind keine fertigen Designs, die direkt in den Code eingefügt werden können, sondern eher Vorlagen, wie bestimmte Probleme in bestimmten Situationen gelöst werden können. Im Folgenden sind einige Beispiele für Design Patterns und wie sie in TypeScript implementiert werden können:

46.1 Singleton-Pattern

Das Singleton-Pattern stellt sicher, dass eine Klasse nur eine einzige Instanz hat und stellt einen globalen Zugriffspunkt zu dieser Instanz bereit. In TypeScript könnte das folgendermaßen aussehen:

class Singleton {
    private static instance: Singleton;

    private constructor() { }

    static getInstance(): Singleton {
        if (!Singleton.instance) {
            Singleton.instance = new Singleton();
        }
        return Singleton.instance;
    }
}

const instance1 = Singleton.getInstance();
const instance2 = Singleton.getInstance();

console.log(instance1 === instance2);  // true

46.2 Factory-Pattern

Das Factory-Pattern ist eine Methode zur Erstellung von Objekten ohne die genaue Klasse der zu erstellenden Objekte zu spezifizieren. In TypeScript könnte das folgendermaßen aussehen:

interface Product {
    doSomething(): void;
}

class ConcreteProductA implements Product {
    doSomething() {
        console.log('ConcreteProductA');
    }
}

class ConcreteProductB implements Product {
    doSomething() {
        console.log('ConcreteProductB');
    }
}

class ProductFactory {
    createProduct(type: 'A' | 'B'): Product {
        if (type === 'A') {
            return new ConcreteProductA();
        } else {
            return new ConcreteProductB();
        }
    }
}

const factory = new ProductFactory();
const productA = factory.createProduct('A');
productA.doSomething();  // Output: 'ConcreteProductA'

46.3 Observer-Pattern

Das Observer-Pattern ist ein Verhaltensmuster, das es einem Objekt (dem Subject) ermöglicht, seinen Zustand an alle abhängigen Objekte (die Observer) automatisch zu melden und zu aktualisieren, wenn dieser Zustand sich ändert. In TypeScript könnte das folgendermaßen aussehen:

interface Observer {
    update(subject: Subject): void;
}

class ConcreteObserver implements Observer {
    update(subject: Subject) {
        console.log(`Observer updated with ${subject.getState()}`);
    }
}

class Subject {
    private observers: Observer[] = [];
    private state: number;

    attach(observer: Observer) {
        this.observers.push(observer);
    }

    setState(state: number) {
        this.state = state;
        this.notify();
    }

    getState(): number {
        return this.state;
    }

    notify() {
        for (const observer of this.observers) {
            observer.update(this);
        }
    }
}

const subject = new Subject();
const observer = new ConcreteObserver();
subject.attach(observer);

subject.setState(123);  // Output: 'Observer updated with 123'

46.4 Decorator-Pattern

Dieses Muster ermöglicht es, das Verhalten von Objekten dynamisch zu erweitern, ohne die ursprüngliche Klasse zu ändern:

class BasicComponent {
    operation(): string {
        return 'BasicComponent';
    }
}

class Decorator extends BasicComponent {
    private component: BasicComponent;

    constructor(component: BasicComponent) {
        super();
        this.component = component;
    }

    operation(): string {
        return `Decorator(${this.component.operation()})`;
    }
}

46.5 Strategy-Pattern

Ermöglicht die Definition einer Familie von Algorithmen, die austauschbar sind, um die Flexibilität in der Wahl des gewünschten Verhaltens zu erhöhen:

interface Strategy {
    execute(data: string): void;
}

class ConcreteStrategyA implements Strategy {
    execute(data: string) {
        console.log(`A: ${data}`);
    }
}

class Context {
    private strategy: Strategy;

    setStrategy(strategy: Strategy) {
        this.strategy = strategy;
    }

    executeStrategy(data: string) {
        this.strategy.execute(data);
    }
}

46.6 Command-Pattern

Wandelt Anfragen oder einfache Operationen in Objekte um, um flexiblere Operationen zu ermöglichen:

interface Command {
    execute(): void;
}

class ConcreteCommand implements Command {
    execute() {
        console.log('Command executed');
    }
}

class Invoker {
    private command: Command;

    setCommand(command: Command) {
        this.command = command;
    }

    run() {
        this.command.execute();
    }
}

46.7 Bridge-Pattern

Trennt die Abstraktion von ihrer Implementierung, sodass beide unabhängig voneinander variiert werden können:

interface Implementor {
    operationImpl(): void;
}

class ConcreteImplementorA implements Implementor {
    operationImpl() {
        console.log('ConcreteImplementorA');
    }
}

class Abstraction {
    protected implementor: Implementor;

    constructor(implementor: Implementor) {
        this.implementor = implementor;
    }

    operation() {
        this.implementor.operationImpl();
    }
}

46.8 Chain of Responsibility-Pattern

Ermöglicht das Durchreichen von Anfragen entlang einer Kette von Handlern, wobei jeder Handler entscheidet, ob er die Anfrage bearbeitet oder an den nächsten Handler in der Kette weiterleitet:

abstract class Handler {
    protected next: Handler;

    setNext(handler: Handler): Handler {
        this.next = handler;
        return handler;
    }

    handle(request: string): void {
        if (this.next) {
            this.next.handle(request);
        }
    }
}

class ConcreteHandler1 extends Handler {
    handle(request: string): void {
        if (request === 'R1') {
            console.log('Handler1 handling R1');
        } else {
            super.handle(request);
        }
    }
}

46.9 Vor- und Nachteile, Anwendungsfälle und Best Practices

Jedes Design Pattern hat spezifische Vor- und Nachteile sowie typische Anwendungsfälle. Zum Beispiel:

Die Anwendung dieser Muster sollte auf den spezifischen Kontext und die Anforderungen Ihres Projekts zugeschnitten sein. Best Practices beinhalten:


Dieser erweiterte Artikel bietet einen umfassenden Überblick über eine Vielzahl von Design Patterns in TypeScript. Er zeigt nicht nur, wie diese Patterns implementiert werden können, sondern diskutiert auch ihre Vor- und Nachteile sowie typische Anwendungsfälle, um Entwicklern zu helfen, fundierte Entscheidungen über ihren Einsatz zu treffen.