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:
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); // trueDas 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'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'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()})`;
}
}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);
}
}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();
}
}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();
}
}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);
}
}
}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.