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));
YouTip