Prefer to listen?
Design patterns are proven solutions to common problems in software design. They provide a template for writing maintainable, scalable, and robust code. Let's take a look at several design patterns and their implementation in modern JavaScript and TypeScript.
The Singleton pattern ensures that a class has only one instance and provides a global point of access to it.
class Singleton {
constructor() {
if (!Singleton.instance) {
Singleton.instance = this;
}
return Singleton.instance;
}
getInstance() {
return this;
}
}
const singleton1 = new Singleton();
const singleton2 = new Singleton();
console.log(singleton1 === singleton2); // Output: true
class Singleton {
private static instance: Singleton;
private constructor() {}
static getInstance(): Singleton {
if (!Singleton.instance) {
Singleton.instance = new Singleton();
}
return Singleton.instance;
}
}
const singleton1 = Singleton.getInstance();
const singleton2 = Singleton.getInstance();
console.log(singleton1 === singleton2); // Output: true
The Factory pattern provides a way to create objects without specifying the exact class of the object that will be created.
class Car {
constructor() {
this.type = 'Car';
}
}
class Truck {
constructor() {
this.type = 'Truck';
}
}
class VehicleFactory {
static createVehicle(vehicleType) {
switch (vehicleType) {
case 'car':
return new Car();
case 'truck':
return new Truck();
default:
throw new Error('Unknown vehicle type');
}
}
}
const car = VehicleFactory.createVehicle('car');
const truck = VehicleFactory.createVehicle('truck');
console.log(car.type); // Output: Car
console.log(truck.type); // Output: Truck
interface Vehicle {
type: string;
}
class Car implements Vehicle {
type = 'Car';
}
class Truck implements Vehicle {
type = 'Truck';
}
class VehicleFactory {
static createVehicle(vehicleType: 'car' | 'truck'): Vehicle {
switch (vehicleType) {
case 'car':
return new Car();
case 'truck':
return new Truck();
default:
throw new Error('Unknown vehicle type');
}
}
}
const car = VehicleFactory.createVehicle('car');
const truck = VehicleFactory.createVehicle('truck');
console.log(car.type); // Output: Car
console.log(truck.type); // Output: Truck
The Observer pattern defines a one-to-many relationship between objects so that when one object changes state, all its dependents are notified and updated automatically.
class Subject {
constructor() {
this.observers = [];
}
addObserver(observer) {
this.observers.push(observer);
}
removeObserver(observer) {
this.observers = this.observers.filter(obs => obs !== observer);
}
notifyObservers() {
this.observers.forEach(observer => observer.update());
}
}
class Observer {
update() {
console.log('Observer has been notified');
}
}
const subject = new Subject();
const observer1 = new Observer();
const observer2 = new Observer();
subject.addObserver(observer1);
subject.addObserver(observer2);
subject.notifyObservers();
// Output:
// Observer has been notified
// Observer has been notified
class Subject {
private observers: Observer[] = [];
addObserver(observer: Observer): void {
this.observers.push(observer);
}
removeObserver(observer: Observer): void {
this.observers = this.observers.filter(obs => obs !== observer);
}
notifyObservers(): void {
this.observers.forEach(observer => observer.update());
}
}
interface Observer {
update(): void;
}
class ConcreteObserver implements Observer {
update(): void {
console.log('Observer has been notified');
}
}
const subject = new Subject();
const observer1 = new ConcreteObserver();
const observer2 = new ConcreteObserver();
subject.addObserver(observer1);
subject.addObserver(observer2);
subject.notifyObservers();
// Output:
// Observer has been notified
// Observer has been notified
The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. This pattern allows the algorithm to vary independently from clients that use it.
class Context {
constructor(strategy) {
this.strategy = strategy;
}
setStrategy(strategy) {
this.strategy = strategy;
}
executeStrategy() {
return this.strategy.doAlgorithm();
}
}
class ConcreteStrategyA {
doAlgorithm() {
return 'Algorithm A';
}
}
class ConcreteStrategyB {
doAlgorithm() {
return 'Algorithm B';
}
}
const context = new Context(new ConcreteStrategyA());
console.log(context.executeStrategy()); // Output: Algorithm A
context.setStrategy(new ConcreteStrategyB());
console.log(context.executeStrategy()); // Output: Algorithm B
interface Strategy {
doAlgorithm(): string;
}
class ConcreteStrategyA implements Strategy {
doAlgorithm(): string {
return 'Algorithm A';
}
}
class ConcreteStrategyB implements Strategy {
doAlgorithm(): string {
return 'Algorithm B';
}
}
class Context {
private strategy: Strategy;
constructor(strategy: Strategy) {
this.strategy = strategy;
}
setStrategy(strategy: Strategy): void {
this.strategy = strategy;
}
executeStrategy(): string {
return this.strategy.doAlgorithm();
}
}
const context = new Context(new ConcreteStrategyA());
console.log(context.executeStrategy()); // Output: Algorithm A
context.setStrategy(new ConcreteStrategyB());
console.log(context.executeStrategy()); // Output: Algorithm B
The Decorator pattern allows behavior to be added to an individual object, dynamically, without affecting the behavior of other objects from the same class.
class Coffee {
cost() {
return 5;
}
}
class MilkDecorator {
constructor(coffee) {
this.coffee = coffee;
}
cost() {
return this.coffee.cost() + 1;
}
}
class SugarDecorator {
constructor(coffee) {
this.coffee = coffee;
}
cost() {
return this.coffee.cost() + 0.5;
}
}
let coffee = new Coffee();
coffee = new MilkDecorator(coffee);
coffee = new SugarDecorator(coffee);
console.log(coffee.cost()); // Output: 6.5
interface Coffee {
cost(): number;
}
class SimpleCoffee implements Coffee {
cost(): number {
return 5;
}
}
class MilkDecorator implements Coffee {
constructor(private coffee: Coffee) {}
cost(): number {
return this.coffee.cost() + 1;
}
}
class SugarDecorator implements Coffee {
constructor(private coffee: Coffee) {}
cost(): number {
return this.coffee.cost() + 0.5;
}
}
let coffee: Coffee = new SimpleCoffee();
coffee = new MilkDecorator(coffee);
coffee = new SugarDecorator(coffee);
console.log(coffee.cost()); // Output: 6.5
Design patterns are essential tools for building robust, scalable, and maintainable software. By understanding and applying these patterns in JavaScript and TypeScript, you can solve common design problems more efficiently and improve the overall quality of your code. Each pattern has its use case and knowing when to apply them is key to leveraging their full potential.
Design patterns are proven solutions to common problems in software design. They provide templates for writing maintainable, scalable, and robust code, helping developers solve recurring issues more efficiently.
The Singleton pattern restricts the instantiation of a class to a single object. It achieves this by maintaining a private static instance of the class and providing a global access method that returns this instance.
The Factory pattern abstracts the instantiation process, allowing the creation of objects without specifying the exact class of the object that will be created. This promotes loose coupling and enhances code maintainability.
The Observer pattern defines a one-to-many relationship between objects. When one object (subject) changes state, all its dependents (observers) are notified and updated automatically. It is commonly used in event-driven programming, such as in UI frameworks.
The Decorator pattern allows you to dynamically add behavior to an object without modifying its class. This provides a flexible alternative to subclassing for extending functionality, enabling more modular and maintainable code.
Battle-Tested Tips for Debugging Django and React Apps
Microservices Architecture with Django and React
TypeScript Best Practices for Large-Scale Applications
Fine-tuning ReactJS State Management for Complex Applications
Advanced Query Techniques in Django's ORM
Key Takeaways from Google IO 2024
Optimising React Applications for Performance
Handling Concurrency in Python
Implementing SEO Best Practices in Django for Better Google Ranking
Top 10 Software Engineering Trends to Watch in 2025
Advanced Testing Techniques in Django
Django Development in 2025
Building Serverless Django Applications