YouTip LogoYouTip

Ts Design Patterns

Design patterns are verified solutions in software development that help us write maintainable and scalable code. TypeScript's type system allows many classic design patterns to be implemented in a type-safe manner. * * * * * * ## Why Design Patterns Are Needed Design patterns are code organization experiences summarized by predecessors, which can solve common software design problems. Using design patterns makes code easier to understand, easier to maintain, and also facilitates team collaboration. TypeScript's type system makes these patterns more robust, with errors being detected at compile time. > **Concept:** A design pattern is a reusable solution to common problems in software design, summarizing code design experience. * * * ## Singleton Pattern Ensure a class has only one instance and provide a global access point. ## Example // Singleton Pattern: Ensuring only one instance class Singleton { // Store singleton instance private static instance: Singleton; private static _data: string =""; // Private constructor to prevent external instantiation private constructor(){} // Static method to get singleton instance public static getInstance(): Singleton { if(!Singleton.instance){ Singleton.instance=new Singleton(); } return Singleton.instance; } // Set data public setData(data: string):void{ Singleton._data = data; } // Get data public getData(): string { return Singleton._data; } } // Test singleton pattern const instance1 = Singleton.getInstance(); const instance2 = Singleton.getInstance(); // Verify it's the same instance console.log("Is the same instance: "+(instance1 === instance2)); instance1.setData("Hello Singleton"); console.log("Data: "+ instance2.getData()); **Output:** Is the same instance: trueData: Hello Singleton > **Private Constructor:** By setting the constructor to private, we prevent external use of new to create instances. * * * ## Factory Pattern Use generic factory to create type-safe object instances. ## Example // Define product interface interface Product { name: string; price: number; getDescription(): string; } // Concrete product: Electronic product class ElectronicProduct implements Product { constructor( public name: string, public price: number, public warranty: number ){} getDescription(): string { return `${this.name}- Β₯${this.price}(warranty ${this.warranty} years)`; } } // Concrete product: Clothing class ClothingProduct implements Product { constructor( public name: string, public price: number, public size: string ){} getDescription(): string { return `${this.name}- Β₯${this.price}(size: ${this.size})`; } } // Factory class class ProductFactory { // Generic factory method static create( type:new(...args: any[])=> T, ...args: any[] ): T { return new type(...args); } } // Use factory to create products const laptop = ProductFactory.create(ElectronicProduct,"Laptop",5999,2); const shirt = ProductFactory.create(ClothingProduct,"T-shirt",199,"L"); console.log(laptop.getDescription()); console.log(shirt.getDescription()); > **Generic Factory:** Using generic constraints ensures returning specific product types. * * * ## Decorator Pattern Use decorators to dynamically add functionality to objects. ## Example // Base coffee interface interface Coffee { getCost(): number; getDescription(): string; } // Base coffee implementation class SimpleCoffee implements Coffee { getCost(): number { return 10; } getDescription(): string { return"Coffee"; } } // Decorator base class abstract class CoffeeDecorator implements Coffee { constructor(protected coffee: Coffee){} getCost(): number { return this.coffee.getCost(); } getDescription(): string { return this.coffee.getDescription(); } } // Milk decorator class MilkDecorator extends CoffeeDecorator { getCost(): number { return this.coffee.getCost()+2; } getDescription(): string { return this.coffee.getDescription()+", Milk"; } } // Sugar decorator class SugarDecorator extends CoffeeDecorator { getCost(): number { return this.coffee.getCost()+1; } getDescription(): string { return this.coffee.getDescription()+", Sugar"; } } // Use decorator let coffee: Coffee =new SimpleCoffee(); console.log(coffee.getDescription()+" - Β₯"+ coffee.getCost()); coffee =new MilkDecorator(coffee); console.log(coffee.getDescription()+" - Β₯"+ coffee.getCost()); coffee =new SugarDecorator(coffee); console.log(coffee.getDescription()+" - Β₯"+ coffee.getCost()); > **Decorator:** Can dynamically add new features without modifying the original class, an experimental feature in TypeScript. * * * ## Observer Pattern Define one-to-many dependencies between objects, so when an object changes state, all dependents are notified. ## Example // Observer interface interface Observer { update(message: string):void; } // Subject interface interface Subject { attach(observer: Observer):void; detach(observer: Observer):void; notify():void; } // Concrete subject: Message center class MessageCenter implements Subject { private observers: Observer[]=[]; private message: string =""; // Add observer attach(observer: Observer):void{ this.observers.push(observer); } // Remove observer detach(observer: Observer):void{ const index =this.observers.indexOf(observer); if(index >-1){ this.observers.splice(index,1); } } // Notify all observers notify():void{ for(const observer of this.observers){ observer.update(this.message); } } // Publish message publish(message: string):void{ this.message= message; console.log("Publishing message: "+ message); this.notify(); } } // Concrete observer: User class UserObserver implements Observer { constructor(public name: string){} update(message: string):void{ console.log(`[${this.name}] received message: ${message}`); } } // Use observer pattern const center =new MessageCenter(); const user1 =new UserObserver("User A"); const user2 =new UserObserver("User B"); center.attach(user1); center.attach(user2); center.publish("New feature is online!"); > **Decoupling:** The observer pattern achieves loose coupling between subjects and observers. * * * ## Strategy Pattern Define a series of algorithms, encapsulate them one by one, and make them interchangeable. ## Example // Payment strategy interface interface PaymentStrategy { pay(amount: number):void; } // WeChat Pay strategy class WechatPayStrategy implements PaymentStrategy { pay(amount: number):void{ console.log(`Paid Β₯${amount} using WeChat`); } } // Alipay strategy class AlipayStrategy implements PaymentStrategy { pay(amount: number):void{ console.log(`Paid Β₯${amount} using Alipay`); } } // Card payment strategy class CardPayStrategy implements PaymentStrategy { pay(amount: number):void{ console.log(`Paid Β₯${amount} using card`); } } // Payment context class PaymentContext { private strategy: PaymentStrategy; constructor(strategy: PaymentStrategy){ this.strategy= strategy; } // Set payment strategy setStrategy(strategy: PaymentStrategy):void{ this.strategy= strategy; } // Execute payment pay(amount: number):void{ this.strategy.pay(amount); } } // Use strategy pattern const context =new PaymentContext(new WechatPayStrategy()); context.pay(100); context.setStrategy(new AlipayStrategy()); context.pay(200); context.setStrategy(new CardPayStrategy()); context.pay(300); > **Algorithm Switching:** The strategy pattern allows switching algorithms at runtime, providing great flexibility. * * * ## Dependency Injection Inject dependencies through constructors, one of the most commonly used patterns in TypeScript. ## Example // Define service interfaces interface Logger { log(message: string):void; } interface Storage { save(key: string, data: any):void; } // Concrete service implementations class ConsoleLogger implements Logger { log(message: string):void{ console.log(": "+ message); } } class LocalStorage implements Storage { save(key: string, data: any):void{ console.log(`Saving ${key}: ${JSON.stringify(data)}`); localStorage.setItem(key, JSON.stringify(data)); } } // Service using dependency injection class UserService { constructor( private logger: Logger, private storage: Storage ){} createUser(name: string):void{ const user ={ name, createdAt:new Date()}; this.logger.log("Created user: "+ name); this.storage.save("user", user); } } // Inject dependencies const logger =new ConsoleLogger(); const storage =new LocalStorage(); const userService =new UserService(logger, storage); userService.createUser("Alice"); > **Dependency Inversion:** Dependency injection realizes that high-level modules do not depend on low-level modules but rather on abstractions. * * * ## Builder Pattern Separate the construction of a complex object from its representation, allowing the same construction process to create different representations. ## Example // Builder interface interface Builder{ build(): T; } // Complex object: User configuration interface UserConfig { name: string; email: string; age?: number; role?: string; theme?: string; } // User configuration builder class UserConfigBuilder implements Builder{ private config: Partial={}; setName(name: string):this{ this.config.name= name; return this; } setEmail(email: string):this{ this.config.email= email; return this; } setAge(age: number):this{ this.config.age= age; return this; } setRole(role: string):this{ this.config.role= role; return this; } setTheme(theme: string):this{ this.config.theme= theme; return this; } build(): UserConfig { if(!this.config.name||!this.config.email){ throw new Error("Name and email are required"); } return this.config as UserConfig; } } // Use builder const builder =new UserConfigBuilder(); const config = builder .setName("Alice") .setEmail("alice@example.com") .setAge(25) .setRole("admin") .setTheme("dark") .build(); console.log("User config:", JSON.stringify(config, null, 2));
← Ts Comprehensive ProjectTs References β†’