Design patterns are a set of best practices used to solve common problems in software development and make writing clean and maintainable code easier. In this blog post, we will discuss some of the most commonly used design patterns in TypeScript.
Singleton
The Singleton pattern is a design pattern that restricts the instantiation of a class to one object and ensures that only one object of a class is created. Implementing the Singleton pattern in TypeScript is very easy:
class Singleton {
private static instance: Singleton;
private constructor() {}
public static getInstance(): Singleton {
if (!Singleton.instance) {
Singleton.instance = new Singleton();
}
return Singleton.instance;
}
}
You can use it like this:
const singleton = Singleton.getInstance();
Factory
The Factory pattern is a design pattern that lets you create objects without specifying the exact class of the object that will be created. In this example, we want to make a vehicle depending on its type, so instead of making a class for each type, we make a single factory class to make us a vehicle depending on the type we give it.
abstract class Vehicle {
abstract getType(): string;
}
class Car extends Vehicle {
getType() {
return 'car';
}
}
class Truck extends Vehicle {
getType() {
return 'truck';
}
}
class VehicleFactory {
public createVehicle(type: string): Vehicle {
switch (type) {
case 'car':
return new Car();
case 'truck':
return new Truck();
default:
throw new Error(`Vehicle of type ${type} not found`);
}
}
}
You can use it like this to make as many vehicles as you want, as long as you provide the type:
const factory = new VehicleFactory();
const car = factory.createVehicle('car');
const truck = factory.createVehicle('truck');
Observer
The Observer pattern is a design pattern that lets you define a subscription mechanism to notify multiple objects and is used in the event-driven programming paradigm. Implementing the Observer pattern in TypeScript can look like this:
interface Observer {
update(data: any): void;
}
class Subject {
private observers: Observer[] = [];
public subscribe(observer: Observer) {
this.observers.push(observer);
}
public unsubscribe(observer: Observer) {
const index = this.observers.indexOf(observer);
this.observers.splice(index, 1);
}
public notify(data: any) {
this.observers.forEach(observer => observer.update(data));
}
}
Then you will need an observer class:
class ConcreteObserver implements Observer {
public update(data: any) {
console.log(data);
}
}
You can let the subject know that there is new data available by subscribing to the observer we created:
const subject = new Subject();
const observer = new ConcreteObserver();
subject.subscribe(observer);
subject.notify('Hello World');
// Unsubscribe the observer from the subject:
subject.unsubscribe(observer);
Command
The Command pattern is a design pattern that lets you encapsulate all information needed to perform an action in one object. Implementing the Command pattern can look like this:
interface Command {
execute(): void;
}
class ConcreteCommand implements Command {
constructor(private receiver: Receiver) {}
public execute() {
this.receiver.action();
}
}
class Receiver {
public action() {
console.log('Action called');
}
}
Then you can use the command module to create a command object and pass it to the invoker:
const receiver = new Receiver();
const command = new ConcreteCommand(receiver);
const invoker = new Invoker();
invoker.setCommand(command);
invoker.execute();
Strategy
The Strategy pattern is a design pattern that lets you define a family of algorithms, encapsulate each one, and make them interchangeable. Implementing the Strategy pattern in TypeScript is very easy and you can start with this Strategy
class:
interface Strategy {
execute(data: any): any;
}
class LastElementStrategy implements Strategy {
public execute(data: []) {
return data[data.length - 1];
}
}
Then you can use it like this:
const strategy = new LastElementStrategy();
const data = [1, 2, 3, 4, 5];
let last = strategy.execute(data);
Template Method
The Template Method pattern is a design pattern that lets you define the skeleton of an algorithm in an operation, deferring some steps to subclasses. For example, you want to make a pizza and you want to make it with tomato sauce and cheese, but the toppings can be different. You can use the Template Method pattern like this:
abstract class Pizza {
public makePizza() {
this.prepareDough();
this.addTomatoSauce();
this.addCheese();
this.addToppings();
this.bakePizza();
}
protected prepareDough() {
console.log('Preparing dough');
}
protected addTomatoSauce() {
console.log('Adding tomato sauce');
}
protected addCheese() {
console.log('Adding cheese');
}
protected abstract addToppings(): void;
protected bakePizza() {
console.log('Baking pizza');
}
}
class PepperoniPizza extends Pizza {
protected addToppings() {
console.log('Adding pepperoni');
}
}
class VegetarianPizza extends Pizza {
protected addToppings() {
console.log('Adding vegetables');
}
}
You can use it like this to make both types of pizza:
const pepperoniPizza = new PepperoniPizza();
pepperoniPizza.makePizza();
const vegetarianPizza = new VegetarianPizza();
vegetarianPizza.makePizza();
Conclusion
These are just a few examples of design patterns that can be used in TypeScript. By using these patterns, you can write better code that is easier to maintain and extend. It's important to note that design patterns should not be used blindly, but rather as tools to solve specific problems in your code.