Design Patterns for Humans: Making Complex Software Simple with TypeScript Snippets (Part 1)
Introduction:
Imagine you're trying to fix something at home maybe a leaky faucet or a loose cabinet door. You’ve never done it before, so you take a guess, fiddle with some tools, maybe even make the problem worse. But if you had a simple step-by-step guide, or someone who had solved this problem before, you’d be done in minutes without any stress.
That’s exactly what design patterns do in software.
Design patterns are like problem-solving blueprints. They’re not code you copy and paste they’re time tested approaches that help you tackle common software challenges. Whether you’re building a signup form, a messaging system, or a game engine, chances are someone has already figured out a smart, reusable way to do it.
And when teams use these patterns, they speak the same language. Saying “Let’s use a Factory Pattern” or “This feels like a good use case for Observer” makes collaboration smoother. It means less explaining and more building.
So, Why Do Patterns Matter?
What This Article Will Cover
We’re not here to make things sound academic or intimidating. We’re going to break down design patterns into four friendly categories:
What Exactly Are Design Patterns?
So, what are design patterns really?
At their core, design patterns are not code. They’re not plugins, libraries, or frameworks either. They’re simply ideas smart ways of solving problems that keep popping up in software development.
Think of them like this:
When a problem is common, and the solution has worked well in many situations, someone gives that solution a name and writes it down. That’s a design pattern.
They’re kind of like recipes in cooking. You don’t have to follow a recipe every time you make pasta, but if someone hands you a tried-and-true method that works every time, why not use it? And once you know the pattern, you can tweak it to fit your taste or in coding terms, your specific project.
Design Patterns Are About:
Where Do They Come From?
In the 1990s, four software engineers Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides wrote a book called Design Patterns: Elements of Reusable Object-Oriented Software. People call them the Gang of Four (GoF).
They looked at tons of real projects and picked out patterns that kept working well across different systems. Their book introduced 23 patterns that became the foundation for most of what we still use today.
But you don’t have to read the book cover to cover. You just need to understand why these patterns exist and how they can help you write better software.
The Four Pattern Families
Design patterns aren’t about memorizing fancy names they’re about solving everyday code problems in smart, reusable ways. Below, we’ll break them down into four human-friendly categories. Each pattern comes with a metaphor, a real-world example, and a Typescript snippet you can actually understand.
Creational Patterns : Smart Ways to Create Objects
“Like choosing how to make a sandwich: do it yourself, use a chef, or always eat the same one.”
Creational patterns help you control how objects are created, so your code doesn’t get stuck doing the same thing over and over in different places. Without these patterns, you risk having code that’s messy, tightly connected, and hard to change later.
Object creation can get messy, repetitive, and tightly coupled. Creational patterns give you clean, flexible ways to build objects. Some example scenarios:
Singleton
"Only ever one. Global, but safely controlled instance."
Think of it like the President of a country. No matter how many times you try to “create” a president, there should only be one recognized at any time.
The Singleton pattern is a creational design pattern that ensures a class has only one instance while providing a global access point to that instance. This is useful when exactly one object is needed to coordinate actions across the system. (like a database connection, logger or configuration manager).
Key Characteristics
Potential Pitfalls
Real-World Use Cases
When to Use
When to Avoid
//Basic Singleton Implementation
class Singleton {
private static instance: Singleton;
// Private constructor to prevent direct construction calls
private constructor() {}
// Static method to get the single instance
public static getInstance(): Singleton {
if (!Singleton.instance) {
Singleton.instance = new Singleton();
}
return Singleton.instance;
}
// Example business logic method
public someBusinessLogic(): void {
console.log("Executing some business logic...");
}
}
// Usage
const instance1 = Singleton.getInstance();
const instance2 = Singleton.getInstance();
console.log(instance1 === instance2); // true - both variables contain the same instance
instance1.someBusinessLogic();
//Singleton with Early Initialization
class EarlySingleton {
private static instance: EarlySingleton = new EarlySingleton();
private constructor() {}
public static getInstance(): EarlySingleton {
return EarlySingleton.instance;
}
public someMethod(): void {
console.log("Early initialized singleton method");
}
}
//Singleton with TypeScript's readonly and static
class ReadonlySingleton {
public static readonly Instance = new ReadonlySingleton();
private constructor() {}
public doSomething(): void {
console.log("Doing something...");
}
}
//Usage
ReadonlySingleton.Instance.doSomething();
//Singleton with Lazy Initialization using a Closure
namespace LazySingleton {
class _Singleton {
public doWork(): void {
console.log("Doing some work");
}
}
let instance: _Singleton;
export function getInstance(): _Singleton {
if (!instance) {
instance = new _Singleton();
}
return instance;
}
}
//Usage
LazySingleton.getInstance().doWork();
//Thread-Safe Singleton (for Node.js/TypeScript)
class ThreadSafeSingleton {
private static instance: ThreadSafeSingleton;
private static lock: boolean = false;
private constructor() {}
public static getInstance(): ThreadSafeSingleton {
if (!ThreadSafeSingleton.instance) {
ThreadSafeSingleton.lock = true;
ThreadSafeSingleton.instance = new ThreadSafeSingleton();
ThreadSafeSingleton.lock = false;
}
while (ThreadSafeSingleton.lock) {
// Wait for the lock to be released
}
return ThreadSafeSingleton.instance;
}
}
Factory Method
"A smart method that creates objects so you don’t have to."
Imagine a pizza shop. You order a pizza by telling the shop the type like "pepperoni" or "veggie" and the shop (factory) knows how to prepare it and hands it to you. You don’t need to know the ingredients or how it’s made. Well factory method is that metaphorical shop.
Instead of directly using new on classes, you use a factory function or class that decides which subclass or variation of an object to create and returns it. You want to create many types of similar objects, but you don't want your code to depend on their concrete classes (to reduce tight coupling). Example: You’re building a notification system, and your factory decides whether to return an EmailNotification, SMSNotification, or PushNotification.
Key Characteristics
Potential Pitfalls
Real-World Use Cases
When to Use
When to Avoid
//Basic structure
interface Product {
operation(): string;
}
abstract class Creator {
public abstract factoryMethod(): Product;
public someOperation(): string {
const product = this.factoryMethod();
return `Creator: ${product.operation()}`;
}
}
class ConcreteProduct1 implements Product {
public operation(): string {
return '{Result of ConcreteProduct1}';
}
}
class ConcreteCreator1 extends Creator {
public factoryMethod(): Product {
return new ConcreteProduct1();
}
}
class ConcreteProduct2 implements Product {
public operation(): string {
return '{Result of ConcreteProduct2}';
}
}
class ConcreteCreator2 extends Creator {
public factoryMethod(): Product {
return new ConcreteProduct2();
}
}
// Client code
function clientCode(creator: Creator) {
console.log(creator.someOperation());
}
console.log('App launched with ConcreteCreator1');
clientCode(new ConcreteCreator1());
console.log('App launched with ConcreteCreator2');
clientCode(new ConcreteCreator2());
interface PaymentMethod {
pay(amount: number): string;
}
class CreditCardPayment implements PaymentMethod {
pay(amount: number): string {
return `Paid $${amount} via Credit Card`;
}
}
class PayPalPayment implements PaymentMethod {
pay(amount: number): string {
return `Paid $${amount} via PayPal`;
}
}
class CryptoPayment implements PaymentMethod {
pay(amount: number): string {
return `Paid $${amount} via Cryptocurrency`;
}
}
abstract class PaymentProcessor {
abstract createPaymentMethod(): PaymentMethod;
processPayment(amount: number): string {
const paymentMethod = this.createPaymentMethod();
return paymentMethod.pay(amount);
}
}
class CreditCardProcessor extends PaymentProcessor {
createPaymentMethod(): PaymentMethod {
return new CreditCardPayment();
}
}
class PayPalProcessor extends PaymentProcessor {
createPaymentMethod(): PaymentMethod {
return new PayPalPayment();
}
}
class CryptoProcessor extends PaymentProcessor {
createPaymentMethod(): PaymentMethod {
return new CryptoPayment();
}
}
// Usage
function processOrder(processor: PaymentProcessor, amount: number) {
console.log(processor.processPayment(amount));
}
processOrder(new CreditCardProcessor(), 100);
processOrder(new PayPalProcessor(), 200);
processOrder(new CryptoProcessor(), 300);
interface Plugin {
initialize(): void;
execute(): string;
}
abstract class PluginFactory {
abstract createPlugin(): Plugin;
loadAndRun(): string {
const plugin = this.createPlugin();
plugin.initialize();
return plugin.execute();
}
}
class AnalyticsPlugin implements Plugin {
initialize() {
console.log('Initializing analytics plugin...');
}
execute(): string {
return 'Analytics data processed';
}
}
class AnalyticsPluginFactory extends PluginFactory {
createPlugin(): Plugin {
return new AnalyticsPlugin();
}
}
class BackupPlugin implements Plugin {
initialize() {
console.log('Initializing backup plugin...');
}
execute(): string {
return 'Backup completed successfully';
}
}
class BackupPluginFactory extends PluginFactory {
createPlugin(): Plugin {
return new BackupPlugin();
}
}
// Dynamic plugin loading
type PluginConstructor = new () => Plugin;
class DynamicPluginFactory extends PluginFactory {
constructor(private pluginClass: PluginConstructor) {
super();
}
createPlugin(): Plugin {
return new this.pluginClass();
}
}
// Usage
const analytics = new AnalyticsPluginFactory();
console.log(analytics.loadAndRun());
const backup = new BackupPluginFactory();
console.log(backup.loadAndRun());
// Dynamic usage
const dynamicFactory = new DynamicPluginFactory(AnalyticsPlugin);
console.log(dynamicFactory.loadAndRun());
Abstract Factory
"A factory of factories."
Imagine a furniture store that produces Victorian and Modern furniture sets. You want to make sure that if someone picks the Victorian theme, they get Victorian Chair, Victorian Sofa, etc., and not a mix of styles.
The Abstract Factory pattern is a creational design pattern that provides an interface for creating families of related or dependent objects without specifying their concrete classes. Example: UI components for different operating systems.
Key Characteristics
Potential Pitfalls
Real-World Use Cases
When to Use
When to Avoid
// Abstract Products
interface Button {
render(): void;
onClick(f: Function): void;
}
interface Checkbox {
render(): void;
toggle(): void;
}
// Abstract Factory
interface GUIFactory {
createButton(): Button;
createCheckbox(): Checkbox;
}
// Concrete Products for Windows
class WindowsButton implements Button {
render() {
console.log("Rendering a Windows style button");
}
onClick(f: Function) {
console.log("Windows button clicked");
f();
}
}
class WindowsCheckbox implements Checkbox {
render() {
console.log("Rendering a Windows style checkbox");
}
toggle() {
console.log("Windows checkbox toggled");
}
}
// Concrete Products for MacOS
class MacOSButton implements Button {
render() {
console.log("Rendering a MacOS style button");
}
onClick(f: Function) {
console.log("MacOS button clicked");
f();
}
}
class MacOSCheckbox implements Checkbox {
render() {
console.log("Rendering a MacOS style checkbox");
}
toggle() {
console.log("MacOS checkbox toggled");
}
}
// Concrete Factories
class WindowsFactory implements GUIFactory {
createButton(): Button {
return new WindowsButton();
}
createCheckbox(): Checkbox {
return new WindowsCheckbox();
}
}
class MacOSFactory implements GUIFactory {
createButton(): Button {
return new MacOSButton();
}
createCheckbox(): Checkbox {
return new MacOSCheckbox();
}
}
// Client code
class Application {
private factory: GUIFactory;
private button: Button;
constructor(factory: GUIFactory) {
this.factory = factory;
this.button = this.factory.createButton();
}
createUI() {
this.button.render();
const checkbox = this.factory.createCheckbox();
checkbox.render();
}
onClick() {
this.button.onClick(() => console.log("Button callback executed"));
}
}
// Usage
function createApp(os: string): Application {
let factory: GUIFactory;
switch(os) {
case "Windows":
factory = new WindowsFactory();
break;
case "MacOS":
factory = new MacOSFactory();
break;
default:
throw new Error("Unknown OS");
}
return new Application(factory);
}
// Create Windows app
const windowsApp = createApp("Windows");
windowsApp.createUI();
windowsApp.onClick();
// Create MacOS app
const macApp = createApp("MacOS");
macApp.createUI();
macApp.onClick();
Builder
"Create complex objects step by step."
Imagine you're at a pizza place:
You want to create a complex object (with lots of optional or nested parts), but you don't want to have a constructor with a million parameters or messy initialization logic.
Think: making a custom pizza, where you choose size, crust, sauce, toppings, etc. Not everyone wants olives!
Key Characteristics
Potential Pitfalls
Real-World Use Cases
When to Use
When to Avoid
//Basic implementation
// Product
class Pizza {
public size: number;
public cheese: boolean;
public pepperoni: boolean;
public bacon: boolean;
public mushrooms: boolean;
constructor(builder: PizzaBuilder) {
this.size = builder.size;
this.cheese = builder.cheese;
this.pepperoni = builder.pepperoni;
this.bacon = builder.bacon;
this.mushrooms = builder.mushrooms;
}
describe(): string {
let description = `This is a ${this.size} inch pizza with:`;
if (this.cheese) description += ' cheese,';
if (this.pepperoni) description += ' pepperoni,';
if (this.bacon) description += ' bacon,';
if (this.mushrooms) description += ' mushrooms,';
return description.slice(0, -1); // Remove trailing comma
}
}
// Builder
class PizzaBuilder {
public size: number;
public cheese: boolean = false;
public pepperoni: boolean = false;
public bacon: boolean = false;
public mushrooms: boolean = false;
constructor(size: number) {
this.size = size;
}
addCheese(): PizzaBuilder {
this.cheese = true;
return this;
}
addPepperoni(): PizzaBuilder {
this.pepperoni = true;
return this;
}
addBacon(): PizzaBuilder {
this.bacon = true;
return this;
}
addMushrooms(): PizzaBuilder {
this.mushrooms = true;
return this;
}
build(): Pizza {
return new Pizza(this);
}
}
// Usage
const myPizza = new PizzaBuilder(14)
.addCheese()
.addPepperoni()
.addMushrooms()
.build();
console.log(myPizza.describe());
// Output: This is a 14 inch pizza with: cheese, pepperoni, mushrooms
//Advanced implementation
// Director
class PizzaChef {
constructMargherita(builder: PizzaBuilder): Pizza {
return builder
.addCheese()
.build();
}
constructPepperoniFeast(builder: PizzaBuilder): Pizza {
return builder
.addCheese()
.addPepperoni()
.addBacon()
.build();
}
constructVegetarian(builder: PizzaBuilder): Pizza {
return builder
.addCheese()
.addMushrooms()
.build();
}
}
// Usage with director
const chef = new PizzaChef();
const builder = new PizzaBuilder(12);
const margherita = chef.constructMargherita(builder);
const pepperoniFeast = chef.constructPepperoniFeast(new PizzaBuilder(16));
console.log(margherita.describe());
// Output: This is a 12 inch pizza with: cheese
console.log(pepperoniFeast.describe());
// Output: This is a 16 inch pizza with: cheese, pepperoni, bacon
Prototype
“Clone an object instead of creating a new one.”
Imagine you're designing a video game. You have a Monster with complex setup logic (AI, animations, etc.). Instead of recreating similar monsters from scratch, you create one base monster and clone it for others with slight tweaks.
The Prototype pattern is a creational design pattern that allows objects to be created by cloning an existing object (prototype) rather than creating new instances from scratch. This is particularly useful when object creation is expensive or complex.
Think of it like "copy-paste and tweak" rather than "build from scratch"
Key Characteristics
Potential Pitfalls
Real-World Use Cases
When to Use
When to Avoid
interface Prototype {
clone(): Prototype;
toString(): string;
}
class ConcretePrototype1 implements Prototype {
private property: string;
constructor(property: string) {
this.property = property;
}
clone(): Prototype {
return new ConcretePrototype1(this.property);
}
setProperty(property: string): void {
this.property = property;
}
toString(): string {
return `ConcretePrototype1 with property: ${this.property}`;
}
}
class ConcretePrototype2 implements Prototype {
private number: number;
constructor(number: number) {
this.number = number;
}
clone(): Prototype {
return new ConcretePrototype2(this.number);
}
setNumber(number: number): void {
this.number = number;
}
toString(): string {
return `ConcretePrototype2 with number: ${this.number}`;
}
}
// Client code
const prototype1 = new ConcretePrototype1("initial");
const clone1 = prototype1.clone();
console.log(clone1.toString()); // ConcretePrototype1 with property: initial
const prototype2 = new ConcretePrototype2(100);
const clone2 = prototype2.clone();
console.log(clone2.toString()); // ConcretePrototype2 with number: 100
//Shallow vs deep copy
class ComplexObject implements Prototype {
public primitive: number;
public object: Date;
public array: number[];
constructor(primitive: number, object: Date, array: number[]) {
this.primitive = primitive;
this.object = object;
this.array = array;
}
// Shallow copy
shallowClone(): Prototype {
return new ComplexObject(this.primitive, this.object, this.array);
}
// Deep copy
deepClone(): Prototype {
return new ComplexObject(
this.primitive,
new Date(this.object.getTime()),
[...this.array]
);
}
}
// Usage
const original = new ComplexObject(1, new Date(), [1, 2, 3]);
const shallowCopy = original.shallowClone();
const deepCopy = original.deepClone();
//Advanced example with Prototype Registry
class PrototypeRegistry {
private prototypes: { [key: string]: Prototype } = {};
addPrototype(key: string, prototype: Prototype): void {
this.prototypes[key] = prototype;
}
getPrototype(key: string): Prototype | undefined {
const prototype = this.prototypes[key];
return prototype?.clone();
}
}
// Usage
const registry = new PrototypeRegistry();
registry.addPrototype("basicEnemy", new ConcretePrototype1("enemy"));
registry.addPrototype("powerfulEnemy", new ConcretePrototype1("super enemy"));
const enemy1 = registry.getPrototype("basicEnemy");
const enemy2 = registry.getPrototype("powerfulEnemy");
Structural Patterns – Building Better Lego Blocks
“Think of plugins, adapters, or extensions that let things fit together better.”
These patterns help you organize your code’s structure, especially when dealing with large systems or pieces that weren’t designed to work together. Some examples scenarios are below:
Adapter
“This doesn’t fit... oh wait, now it does!”
You’re traveling from the US to Europe. Your laptop plug (US) doesn't fit the power socket (EU). What do you do? You use a plug adapter that converts the US plug to fit the EU socket.
The Adapter pattern is a structural design pattern that allows objects with incompatible interfaces to collaborate. It acts as a bridge between two incompatible interfaces by converting the interface of one class into another interface that clients expect. Example: A new charger that needs to fit an old power outlet.
Key Characteristics
Potential Pitfalls
Real-World Use Cases
When to Use
When to Avoid
// Object adapter
// Target interface that clients expect
interface MediaPlayer {
play(audioType: string, fileName: string): void;
}
// Adaptee - existing class with incompatible interface
class AdvancedMediaPlayer {
playVlc(fileName: string): void {
console.log(`Playing vlc file: ${fileName}`);
}
playMp4(fileName: string): void {
console.log(`Playing mp4 file: ${fileName}`);
}
}
// Adapter that implements the target interface
class MediaAdapter implements MediaPlayer {
private advancedPlayer: AdvancedMediaPlayer;
constructor() {
this.advancedPlayer = new AdvancedMediaPlayer();
}
play(audioType: string, fileName: string): void {
if (audioType === 'vlc') {
this.advancedPlayer.playVlc(fileName);
} else if (audioType === 'mp4') {
this.advancedPlayer.playMp4(fileName);
} else {
throw new Error(`Unsupported media type: ${audioType}`);
}
}
}
// Client code
class AudioPlayer implements MediaPlayer {
private mediaAdapter: MediaAdapter;
play(audioType: string, fileName: string): void {
if (audioType === 'mp3') {
console.log(`Playing mp3 file: ${fileName}`);
} else if (audioType === 'vlc' || audioType === 'mp4') {
this.mediaAdapter = new MediaAdapter();
this.mediaAdapter.play(audioType, fileName);
} else {
throw new Error(`Invalid media type: ${audioType}`);
}
}
}
// Usage
const player = new AudioPlayer();
player.play('mp3', 'song.mp3');
player.play('mp4', 'movie.mp4');
player.play('vlc', 'video.vlc');
//Class adapter
// Target interface
interface CelsiusThermometer {
getTemperature(): number;
}
// Adaptee
class FahrenheitThermometer {
getFahrenheitTemp(): number {
return 32; // example value
}
}
// Class Adapter (using inheritance)
class ThermometerAdapter extends FahrenheitThermometer implements CelsiusThermometer {
getTemperature(): number {
return (this.getFahrenheitTemp() - 32) * 5/9;
}
}
// Usage
const celsiusThermometer: CelsiusThermometer = new ThermometerAdapter();
console.log(`Temperature in Celsius: ${celsiusThermometer.getTemperature()}`);
//Advanced TypeScript Implementation with Generics
interface DataReader<T> {
read(): T;
}
interface DataWriter<T> {
write(data: T): void;
}
// Adaptee - CSV processor
class CsvProcessor {
readCsv(): string[][] {
return [["name", "age"], ["Alice", "30"], ["Bob", "25"]];
}
writeCsv(data: string[][]): void {
console.log("Writing CSV:", data);
}
}
// Generic Adapter
class CsvAdapter<T> implements DataReader<T[]>, DataWriter<T[]> {
private csvProcessor: CsvProcessor;
private parseFn: (row: string[]) => T;
private serializeFn: (item: T) => string[];
constructor(
parseFn: (row: string[]) => T,
serializeFn: (item: T) => string[]
) {
this.csvProcessor = new CsvProcessor();
this.parseFn = parseFn;
this.serializeFn = serializeFn;
}
read(): T[] {
const csvData = this.csvProcessor.readCsv();
const [_, ...rows] = csvData; // Skip header
return rows.map(this.parseFn);
}
write(data: T[]): void {
const header = Object.keys(data[0] as object);
const rows = data.map(this.serializeFn);
this.csvProcessor.writeCsv([header, ...rows]);
}
}
// Usage
interface Person {
name: string;
age: number;
}
const personAdapter = new CsvAdapter<Person>(
([name, age]) => ({ name, age: parseInt(age) }),
(person) => [person.name, person.age.toString()]
);
const people = personAdapter.read();
console.log(people); // [{name: "Alice", age: 30}, {name: "Bob", age: 25}]
personAdapter.write([...people, { name: "Charlie", age: 35 }]);
Decorator
"You wrap an object with another object that adds new behavior."
Think of a basic cake. You can add frosting, sprinkles, and candles all without changing the original cake. You’re just decorating it.
The Decorator pattern is a structural design pattern that allows behavior to be added to individual objects dynamically, without affecting the behavior of other objects from the same class. Example: A basic coffee becomes a fancy latte by adding milk, then sugar.
Key Characteristics
Potential Pitfalls
Real-World Use Cases
When to Use
When to Avoid
// Component interface
interface Coffee {
cost(): number;
description(): string;
}
// Concrete Component
class SimpleCoffee implements Coffee {
cost(): number {
return 5;
}
description(): string {
return "Simple coffee";
}
}
// Base Decorator
abstract class CoffeeDecorator implements Coffee {
protected decoratedCoffee: Coffee;
constructor(coffee: Coffee) {
this.decoratedCoffee = coffee;
}
cost(): number {
return this.decoratedCoffee.cost();
}
description(): string {
return this.decoratedCoffee.description();
}
}
// Concrete Decorators
class MilkDecorator extends CoffeeDecorator {
cost(): number {
return this.decoratedCoffee.cost() + 2;
}
description(): string {
return this.decoratedCoffee.description() + ", with milk";
}
}
class WhipDecorator extends CoffeeDecorator {
cost(): number {
return this.decoratedCoffee.cost() + 3;
}
description(): string {
return this.decoratedCoffee.description() + ", with whip";
}
}
class VanillaDecorator extends CoffeeDecorator {
cost(): number {
return this.decoratedCoffee.cost() + 4;
}
description(): string {
return this.decoratedCoffee.description() + ", with vanilla";
}
}
// Usage
let coffee: Coffee = new SimpleCoffee();
console.log(coffee.description(), coffee.cost());
coffee = new MilkDecorator(coffee);
console.log(coffee.description(), coffee.cost());
coffee = new WhipDecorator(coffee);
console.log(coffee.description(), coffee.cost());
coffee = new VanillaDecorator(coffee);
console.log(coffee.description(), coffee.cost());
// Class decorator
function sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}
// Method decorator
function log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function(...args: any[]) {
console.log(`Called ${propertyKey} with args: ${JSON.stringify(args)}`);
const result = originalMethod.apply(this, args);
console.log(`Method ${propertyKey} returned: ${JSON.stringify(result)}`);
return result;
};
return descriptor;
}
@sealed
class Calculator {
@log
add(a: number, b: number): number {
return a + b;
}
}
const calc = new Calculator();
calc.add(2, 3);
interface DataService {
fetchData(id: string): Promise<any>;
}
class RealDataService implements DataService {
async fetchData(id: string): Promise<any> {
// Simulate network request
console.log(`Fetching data for ID: ${id}`);
await new Promise(resolve => setTimeout(resolve, 1000));
return { id, data: `Data for ${id}` };
}
}
class CachingDecorator implements DataService {
private cache: Map<string, Promise<any>> = new Map();
private wrapped: DataService;
constructor(service: DataService) {
this.wrapped = service;
}
fetchData(id: string): Promise<any> {
if (!this.cache.has(id)) {
this.cache.set(id, this.wrapped.fetchData(id));
}
return this.cache.get(id)!;
}
}
// Usage
const service: DataService = new CachingDecorator(new RealDataService());
// First call - will fetch
service.fetchData('1').then(console.log);
// Second call - will use cache
setTimeout(() => {
service.fetchData('1').then(console.log);
}, 2000);
Composite
"Treat groups of objects like a single object."
Think of a folder (directory) in your computer:
This is what the Composite Pattern models: parts and wholes having the same interface.
The Composite pattern is a structural design pattern that lets you compose objects into tree structures and then work with these structures as if they were individual objects. Example: A file system where files and folders can be handled the same way.
Key Characteristics
Potential Pitfalls
Real-World Use Cases
When to Use
When to Avoid
// Component Interface
interface Graphic {
move(x: number, y: number): void;
draw(): void;
}
//Leaf Class (represents individual objects)
class Dot implements Graphic {
constructor(private x: number, private y: number) {}
move(x: number, y: number): void {
this.x += x;
this.y += y;
}
draw(): void {
console.log(`Drawing dot at (${this.x}, ${this.y})`);
}
}
class Circle implements Graphic {
constructor(private x: number, private y: number, private radius: number) {}
move(x: number, y: number): void {
this.x += x;
this.y += y;
}
draw(): void {
console.log(`Drawing circle at (${this.x}, ${this.y}) with radius ${this.radius}`);
}
}
//Composite Class (represents complex components)
class CompoundGraphic implements Graphic {
private children: Graphic[] = [];
add(child: Graphic): void {
this.children.push(child);
}
remove(child: Graphic): void {
const index = this.children.indexOf(child);
if (index !== -1) {
this.children.splice(index, 1);
}
}
move(x: number, y: number): void {
for (const child of this.children) {
child.move(x, y);
}
}
draw(): void {
console.log("Drawing compound graphic:");
for (const child of this.children) {
child.draw();
}
console.log("Finished drawing compound graphic");
}
}
//Client Code
function clientCode() {
const dot1 = new Dot(1, 2);
const dot2 = new Dot(3, 4);
const circle = new Circle(5, 6, 10);
const compound1 = new CompoundGraphic();
compound1.add(dot1);
compound1.add(dot2);
compound1.add(circle);
const dot3 = new Dot(7, 8);
const compound2 = new CompoundGraphic();
compound2.add(dot3);
compound2.add(compound1);
console.log("Drawing all graphics:");
compound2.draw();
console.log("\nMoving all graphics by (10, 10):");
compound2.move(10, 10);
compound2.draw();
}
clientCode();
Facade
"Hide the mess behind a simple door."
Think of a universal remote control. Instead of pressing 10 buttons on your TV, speakers, and DVD player individually, you just press one button on the remote — it takes care of the rest.
The Facade pattern is a structural design pattern that provides a simplified interface to a complex subsystem, library, or framework. It hides the complexities of the underlying system and provides a cleaner, more understandable API to the client. Example: A remote control that hides the complexity of the TV’s internals.
Key Characteristics
Potential Pitfalls
Real-World Use Cases
When to Use
When to Avoid
// Subsystem components
class BluRayPlayer {
turnOn() {
console.log('BluRay player turning on...');
}
turnOff() {
console.log('BluRay player turning off...');
}
play(movie: string) {
console.log(`Playing movie: ${movie}`);
}
}
class SoundSystem {
turnOn() {
console.log('Sound system turning on...');
}
turnOff() {
console.log('Sound system turning off...');
}
setVolume(level: number) {
console.log(`Setting volume to ${level}`);
}
}
class Projector {
turnOn() {
console.log('Projector turning on...');
}
turnOff() {
console.log('Projector turning off...');
}
setInput(source: string) {
console.log(`Setting input to ${source}`);
}
}
class Lights {
dim(level: number) {
console.log(`Dimming lights to ${level}%`);
}
brighten() {
console.log('Bringing lights back to normal');
}
}
// Facade
class HomeTheaterFacade {
private bluRay: BluRayPlayer;
private sound: SoundSystem;
private projector: Projector;
private lights: Lights;
constructor(bluRay: BluRayPlayer, sound: SoundSystem, projector: Projector, lights: Lights) {
this.bluRay = bluRay;
this.sound = sound;
this.projector = projector;
this.lights = lights;
}
watchMovie(movie: string) {
console.log('Getting ready to watch a movie...');
this.lights.dim(10);
this.projector.turnOn();
this.projector.setInput('bluray');
this.sound.turnOn();
this.sound.setVolume(50);
this.bluRay.turnOn();
this.bluRay.play(movie);
}
endMovie() {
console.log('Shutting down the home theater...');
this.bluRay.turnOff();
this.sound.turnOff();
this.projector.turnOff();
this.lights.brighten();
}
}
// Client code
const bluRay = new BluRayPlayer();
const sound = new SoundSystem();
const projector = new Projector();
const lights = new Lights();
const homeTheater = new HomeTheaterFacade(bluRay, sound, projector, lights);
homeTheater.watchMovie('Inception');
// Later...
homeTheater.endMovie();
//Advanced facade
// Complex API subsystem
class AuthService {
login(username: string, password: string): string {
console.log(`Authenticating user: ${username}`);
return 'auth-token-123';
}
}
class DataService {
fetchData(token: string, endpoint: string): any {
console.log(`Fetching data from ${endpoint} with token ${token}`);
return { data: 'some data' };
}
}
class CacheService {
cacheData(key: string, data: any): void {
console.log(`Caching data with key ${key}`);
}
getCachedData(key: string): any {
console.log(`Retrieving cached data for key ${key}`);
return null;
}
}
// Facade
class ApiFacade {
private auth: AuthService;
private data: DataService;
private cache: CacheService;
constructor() {
this.auth = new AuthService();
this.data = new DataService();
this.cache = new CacheService();
}
async getData(username: string, password: string, endpoint: string): Promise<any> {
try {
// Check cache first
const cached = this.cache.getCachedData(endpoint);
if (cached) return cached;
// Authenticate
const token = this.auth.login(username, password);
// Fetch data
const data = this.data.fetchData(token, endpoint);
// Cache the data
this.cache.cacheData(endpoint, data);
return data;
} catch (error) {
console.error('API Error:', error);
throw error;
}
}
}
// Client code
const api = new ApiFacade();
async function fetchUserData() {
const data = await api.getData('user1', 'password123', '/users');
console.log('Received data:', data);
}
fetchUserData();
Bridge
"Separate the 'what' from the 'how'"
Imagine you're designing remote controls for TVs. You could have:
And TVs like:
Instead of creating classes like SamsungBasicRemote, SamsungAdvancedRemote, SonyBasicRemote, etc., you bridge the remote from the TV making them independent of each other.
The Bridge Pattern is a structural design pattern that decouples an abstraction from its implementation so the two can vary independently. This is useful when you have multiple dimensions that might change independently like shapes and rendering APIs, or devices and communication methods.
Abstraction → Implementation
↑ ↑
RefinedAbstraction ConcreteImplementation
Key Characteristics
Potential Pitfalls
Real-World Use Cases
When to Use
When to Avoid
// Implementation interface
interface Device {
isEnabled(): boolean;
enable(): void;
disable(): void;
getVolume(): number;
setVolume(percent: number): void;
getChannel(): number;
setChannel(channel: number): void;
}
// Concrete Implementations
class TV implements Device {
private on = false;
private volume = 30;
private channel = 1;
isEnabled(): boolean {
return this.on;
}
enable(): void {
this.on = true;
}
disable(): void {
this.on = false;
}
getVolume(): number {
return this.volume;
}
setVolume(percent: number): void {
this.volume = percent;
}
getChannel(): number {
return this.channel;
}
setChannel(channel: number): void {
this.channel = channel;
}
}
class Radio implements Device {
private on = false;
private volume = 50;
private channel = 100.5;
isEnabled(): boolean {
return this.on;
}
enable(): void {
this.on = true;
}
disable(): void {
this.on = false;
}
getVolume(): number {
return this.volume;
}
setVolume(percent: number): void {
this.volume = percent;
}
getChannel(): number {
return this.channel;
}
setChannel(channel: number): void {
this.channel = channel;
}
}
// Abstraction
class RemoteControl {
protected device: Device;
constructor(device: Device) {
this.device = device;
}
togglePower(): void {
if (this.device.isEnabled()) {
this.device.disable();
} else {
this.device.enable();
}
}
volumeDown(): void {
this.device.setVolume(this.device.getVolume() - 10);
}
volumeUp(): void {
this.device.setVolume(this.device.getVolume() + 10);
}
channelDown(): void {
this.device.setChannel(this.device.getChannel() - 1);
}
channelUp(): void {
this.device.setChannel(this.device.getChannel() + 1);
}
}
// Refined Abstraction
class AdvancedRemoteControl extends RemoteControl {
mute(): void {
this.device.setVolume(0);
}
record(): void {
console.log("Recording current channel");
}
}
// Usage
const tv = new TV();
const remote = new RemoteControl(tv);
remote.togglePower();
remote.volumeUp();
const radio = new Radio();
const advancedRemote = new AdvancedRemoteControl(radio);
advancedRemote.mute();
//Advanced Example with Generics
// Implementation interface with generics
interface Renderer<T> {
renderShape(shape: T): string;
}
// Concrete Implementations
class VectorRenderer implements Renderer<Shape> {
renderShape(shape: Shape): string {
return `Drawing ${shape.getName()} as vector graphics`;
}
}
class RasterRenderer implements Renderer<Shape> {
renderShape(shape: Shape): string {
return `Drawing ${shape.getName()} as pixels`;
}
}
// Abstraction
abstract class Shape {
protected renderer: Renderer<Shape>;
constructor(renderer: Renderer<Shape>) {
this.renderer = renderer;
}
abstract draw(): string;
abstract getName(): string;
}
// Refined Abstractions
class Circle extends Shape {
constructor(renderer: Renderer<Shape>) {
super(renderer);
}
draw(): string {
return this.renderer.renderShape(this);
}
getName(): string {
return "Circle";
}
}
class Square extends Shape {
constructor(renderer: Renderer<Shape>) {
super(renderer);
}
draw(): string {
return this.renderer.renderShape(this);
}
getName(): string {
return "Square";
}
}
// Usage
const vectorRenderer = new VectorRenderer();
const rasterRenderer = new RasterRenderer();
const circle = new Circle(vectorRenderer);
console.log(circle.draw()); // "Drawing Circle as vector graphics"
const square = new Square(rasterRenderer);
console.log(square.draw()); // "Drawing Square as pixels"
Flyweight
"Share objects to save memory."
Imagine a text editor rendering a massive document. Each character has formatting like font, size, and style. Instead of creating a new object for every character, we can reuse the same objects for characters with the same formatting.
The Flyweight pattern is a structural design pattern that minimizes memory usage by sharing as much data as possible with similar objects. It's particularly useful when you need to create a large number of similar objects that would otherwise consume too much memory.
Key Characteristics
Potential Pitfalls
Real-World Use Cases
When to Use
When to Avoid
// Flyweight interface
interface Flyweight {
operation(extrinsicState: string): void;
}
// Concrete Flyweight
class ConcreteFlyweight implements Flyweight {
private intrinsicState: string;
constructor(intrinsicState: string) {
this.intrinsicState = intrinsicState;
}
operation(extrinsicState: string): void {
console.log(`ConcreteFlyweight: Intrinsic (${this.intrinsicState}) and Extrinsic (${extrinsicState})`);
}
}
// Flyweight Factory
class FlyweightFactory {
private flyweights: {[key: string]: Flyweight} = {};
getFlyweight(key: string): Flyweight {
if (!(key in this.flyweights)) {
console.log(`Creating new flyweight for key: ${key}`);
this.flyweights[key] = new ConcreteFlyweight(key);
}
return this.flyweights[key];
}
countFlyweights(): number {
return Object.keys(this.flyweights).length;
}
}
// Client code
const factory = new FlyweightFactory();
function addCharacter(factory: FlyweightFactory, char: string, font: string, size: number) {
const flyweight = factory.getFlyweight(char);
flyweight.operation(`Font: ${font}, Size: ${size}`);
}
// Simulate text processing
addCharacter(factory, 'A', 'Helvetica', 12);
addCharacter(factory, 'B', 'Times New Roman', 14);
addCharacter(factory, 'A', 'Helvetica', 12); // Reuses existing 'A' flyweight
addCharacter(factory, 'C', 'Arial', 16);
addCharacter(factory, 'B', 'Verdana', 14); // Reuses existing 'B' flyweight
console.log(`Total flyweights created: ${factory.countFlyweights()}`);
//Advanced example: game tree
// Tree types - intrinsic state
enum TreeType {
OAK,
MAPLE,
PINE
}
// Flyweight
class TreeModel {
constructor(
public type: TreeType,
public mesh: string, // 3D model data
public texture: string
) {}
render(x: number, y: number, height: number): void {
console.log(`Rendering ${TreeType[this.type]} tree at (${x}, ${y}) with height ${height}`);
}
}
// Flyweight Factory
class TreeModelFactory {
private static cache: Map<TreeType, TreeModel> = new Map();
static getTreeModel(type: TreeType): TreeModel {
if (!this.cache.has(type)) {
// In a real app, these would be loaded from files
const mesh = `${TreeType[type]}_mesh.obj`;
const texture = `${TreeType[type]}_texture.png`;
this.cache.set(type, new TreeModel(type, mesh, texture));
console.log(`Created new TreeModel for ${TreeType[type]}`);
}
return this.cache.get(type)!;
}
}
// Context - contains extrinsic state
class Tree {
constructor(
private model: TreeModel,
private x: number,
private y: number,
private height: number
) {}
render(): void {
this.model.render(this.x, this.y, this.height);
}
}
// Client code
const forest: Tree[] = [];
// Create 1000 trees with only 3 models
for (let i = 0; i < 1000; i++) {
const type = Math.floor(Math.random() * 3); // Random tree type
const treeModel = TreeModelFactory.getTreeModel(type);
const tree = new Tree(
treeModel,
Math.random() * 1000, // x
Math.random() * 1000, // y
5 + Math.random() * 20 // height
);
forest.push(tree);
}
// Render all trees
forest.forEach(tree => tree.render());
console.log(`Total tree models created: ${TreeModelFactory['cache'].size}`);
Proxy
"A middleman who controls access."
Think of it like a personal assistant: the assistant can handle tasks for you, decide who gets access to you, or log interactions on your behalf.
The Proxy Pattern is a structural design pattern that provides a surrogate or placeholder for another object to control access to it. Example: A security guard standing in for access to a private vault.
Key Characteristics
Types of Proxies
Potential Pitfalls
Real-World Use Cases
When to Use
When to Avoid
// Subject interface
interface Image {
display(): void;
}
// RealSubject
class RealImage implements Image {
private filename: string;
constructor(filename: string) {
this.filename = filename;
this.loadFromDisk();
}
private loadFromDisk(): void {
console.log(`Loading image: ${this.filename}`);
}
display(): void {
console.log(`Displaying image: ${this.filename}`);
}
}
// Proxy
class ImageProxy implements Image {
private realImage: RealImage | null = null;
private filename: string;
constructor(filename: string) {
this.filename = filename;
}
display(): void {
if (this.realImage === null) {
this.realImage = new RealImage(this.filename);
}
this.realImage.display();
}
}
// Client code
const image1: Image = new ImageProxy("photo1.jpg");
const image2: Image = new ImageProxy("photo2.jpg");
// Image will be loaded only when display() is called
image1.display(); // Loads and displays
image1.display(); // Only displays (already loaded)
image2.display(); // Loads and displays
//Advanced Example: Protection Proxy
// Subject interface
interface Database {
query(sql: string): any[];
}
// RealSubject
class RealDatabase implements Database {
query(sql: string): any[] {
console.log(`Executing query: ${sql}`);
// Actual database query logic would go here
return [{ id: 1, name: "Example Data" }];
}
}
// Proxy
class DatabaseProxy implements Database {
private realDatabase: RealDatabase;
private userRole: string;
constructor(userRole: string) {
this.realDatabase = new RealDatabase();
this.userRole = userRole;
}
private isAllowed(query: string): boolean {
// Simple authorization check
if (this.userRole === "admin") return true;
if (query.includes("DELETE") || query.includes("UPDATE")) {
return false;
}
return true;
}
query(sql: string): any[] {
if (!this.isAllowed(sql)) {
throw new Error("Access denied: You don't have permission to execute this query");
}
console.log(`Logging query by ${this.userRole}: ${sql}`);
return this.realDatabase.query(sql);
}
}
// Client code
const adminDb: Database = new DatabaseProxy("admin");
const userDb: Database = new DatabaseProxy("user");
console.log(adminDb.query("SELECT * FROM users")); // Works
console.log(userDb.query("SELECT * FROM products")); // Works
console.log(userDb.query("DELETE FROM users WHERE id = 1")); // Throws error
Behavioral Patterns – Making Your Code Communicate
“Like social rules for your code: who talks to whom, and how.”
How different parts of your app talk to each other without causing chaos or being too tightly coupled. These patterns manage interactions between objects, ensuring they collaborate cleanly and flexibly. Example scenarios are below:
Observer
“Let me know when that changes.”
Imagine you're subscribed to a newsletter:
The Observer Pattern is like a group chat.
In software:
Example: Subscribing to changes in a data store or UI state.
Key Characteristics
Potential Pitfalls
Real-World Use Cases
When to Use
When to Avoid
// Observer interface
interface Observer {
update(data: any): void;
}
// Subject (Observable) interface
interface Subject {
subscribe(observer: Observer): void;
unsubscribe(observer: Observer): void;
notify(data: any): void;
}
// Concrete Subject
class NewsAgency implements Subject {
private observers: Observer[] = [];
private latestNews: string = '';
subscribe(observer: Observer): void {
if (!this.observers.includes(observer)) {
this.observers.push(observer);
}
}
unsubscribe(observer: Observer): void {
const index = this.observers.indexOf(observer);
if (index !== -1) {
this.observers.splice(index, 1);
}
}
notify(data: any): void {
for (const observer of this.observers) {
observer.update(data);
}
}
setNews(news: string): void {
this.latestNews = news;
this.notify(news);
}
getLatestNews(): string {
return this.latestNews;
}
}
// Concrete Observer
class NewsChannel implements Observer {
private name: string;
private latestNews: string = '';
constructor(name: string) {
this.name = name;
}
update(data: any): void {
this.latestNews = data;
this.display();
}
display(): void {
console.log(`${this.name} received news: ${this.latestNews}`);
}
}
// Usage
const bbc = new NewsChannel('BBC');
const cnn = new NewsChannel('CNN');
const newsAgency = new NewsAgency();
newsAgency.subscribe(bbc);
newsAgency.subscribe(cnn);
newsAgency.setNews('Breaking News: TypeScript 5.0 released!');
// Output:
// BBC received news: Breaking News: TypeScript 5.0 released!
// CNN received news: Breaking News: TypeScript 5.0 released!
newsAgency.unsubscribe(cnn);
newsAgency.setNews('Update: New features in TS 5.0');
// Output:
// BBC received news: Update: New features in TS 5.0
//Advanced implementation with generics
// Generic Observable implementation
class Observable<T> {
private observers: ((value: T) => void)[] = [];
subscribe(observer: (value: T) => void): () => void {
this.observers.push(observer);
return () => {
this.observers = this.observers.filter(obs => obs !== observer);
};
}
notify(value: T): void {
for (const observer of this.observers) {
observer(value);
}
}
}
// Usage example
const temperatureObservable = new Observable<number>();
const unsubscribe = temperatureObservable.subscribe(temp => {
console.log(`Current temperature: ${temp}°C`);
});
temperatureObservable.notify(23.5); // Logs: Current temperature: 23.5°C
temperatureObservable.notify(24.1); // Logs: Current temperature: 24.1°C
// Later...
unsubscribe(); // Stop receiving updates
Strategy
“Swap logic without changing the object.”
Imagine a character in a game that can attack using a sword, bow, or magic. Instead of writing separate classes for each type of character, you use the Strategy Pattern to inject the attack behavior dynamically. Example: Different payment methods (credit card, PayPal, Bitcoin) in a checkout process.
The Strategy Pattern is a behavioral design pattern that enables selecting an algorithm's behavior at runtime. It defines a family of algorithms, encapsulates each one, and makes them interchangeable. This pattern is particularly useful when you have multiple ways to perform an action and want to switch between them easily without changing the code that uses the algorithm.
Key Characteristics
Potential Pitfalls
Real-World Use Cases
When to Use
When to Avoid
// Strategy Interface
interface PaymentStrategy {
pay(amount: number): void;
}
// Concrete Strategies
class CreditCardPayment implements PaymentStrategy {
constructor(private cardNumber: string, private cvv: string) {}
pay(amount: number): void {
console.log(`Paying $${amount} using credit card ${this.cardNumber.substring(0, 4)}...`);
// Actual credit card payment logic would go here
}
}
class PayPalPayment implements PaymentStrategy {
constructor(private email: string) {}
pay(amount: number): void {
console.log(`Paying $${amount} using PayPal account ${this.email}`);
// Actual PayPal payment logic would go here
}
}
class BankTransferPayment implements PaymentStrategy {
constructor(private accountNumber: string) {}
pay(amount: number): void {
console.log(`Paying $${amount} via bank transfer to account ${this.accountNumber}`);
// Actual bank transfer logic would go here
}
}
// Context Class
class ShoppingCart {
private paymentStrategy: PaymentStrategy;
constructor(private items: {name: string, price: number}[] = []) {}
setPaymentStrategy(strategy: PaymentStrategy): void {
this.paymentStrategy = strategy;
}
checkout(): void {
const total = this.items.reduce((sum, item) => sum + item.price, 0);
if (!this.paymentStrategy) {
throw new Error("Payment strategy not set");
}
this.paymentStrategy.pay(total);
}
addItem(item: {name: string, price: number}): void {
this.items.push(item);
}
}
// Usage
const cart = new ShoppingCart();
cart.addItem({name: "Laptop", price: 999});
cart.addItem({name: "Mouse", price: 25});
// Select payment strategy at runtime
cart.setPaymentStrategy(new CreditCardPayment("1234567890123456", "123"));
cart.checkout();
// Change strategy
cart.setPaymentStrategy(new PayPalPayment("user@example.com"));
cart.checkout();
// Generic Strategy Interface
interface TransformationStrategy<T, R> {
execute(input: T): R;
}
// Concrete Strategies
class StringToUpperCase implements TransformationStrategy<string, string> {
execute(input: string): string {
return input.toUpperCase();
}
}
class StringToLowerCase implements TransformationStrategy<string, string> {
execute(input: string): string {
return input.toLowerCase();
}
}
class StringReverse implements TransformationStrategy<string, string> {
execute(input: string): string {
return input.split('').reverse().join('');
}
}
// Context Class with Dependency Injection
class TextProcessor {
constructor(private strategy: TransformationStrategy<string, string>) {}
setStrategy(strategy: TransformationStrategy<string, string>): void {
this.strategy = strategy;
}
process(text: string): string {
return this.strategy.execute(text);
}
}
// Usage
const processor = new TextProcessor(new StringToUpperCase());
console.log(processor.process("Hello Strategy!")); // "HELLO STRATEGY!"
processor.setStrategy(new StringReverse());
console.log(processor.process("Hello Strategy!")); // "!yegarteS olleH"
Command
"Encapsulate actions as objects."
The Command Pattern turns a request into a standalone object that contains all the information about the request — what needs to be done, who should do it, and when. This transformation lets you parameterize methods with different requests, delay or queue a request's execution, and support undoable operations.
It decouples sender (invoker) from the receiver (executor) of a request. Example: Undo/redo systems in a text editor.
Key Characteristics
Potential Pitfalls
Real-World Use Cases
When to Use
When to Avoid
// Command interface
interface Command {
execute(): void;
undo?(): void; // Optional for undo functionality
}
// Receiver - knows how to perform the operations
class Light {
turnOn(): void {
console.log("Light is ON");
}
turnOff(): void {
console.log("Light is OFF");
}
}
// Concrete Command
class TurnOnLightCommand implements Command {
private light: Light;
constructor(light: Light) {
this.light = light;
}
execute(): void {
this.light.turnOn();
}
undo(): void {
this.light.turnOff();
}
}
// Concrete Command
class TurnOffLightCommand implements Command {
private light: Light;
constructor(light: Light) {
this.light = light;
}
execute(): void {
this.light.turnOff();
}
undo(): void {
this.light.turnOn();
}
}
// Invoker - asks the command to carry out the request
class RemoteControl {
private command: Command | null = null;
setCommand(command: Command): void {
this.command = command;
}
pressButton(): void {
if (this.command) {
this.command.execute();
}
}
pressUndo(): void {
if (this.command && this.command.undo) {
this.command.undo();
}
}
}
// Client code
const light = new Light();
const remote = new RemoteControl();
const turnOn = new TurnOnLightCommand(light);
const turnOff = new TurnOffLightCommand(light);
remote.setCommand(turnOn);
remote.pressButton(); // Light is ON
remote.pressUndo(); // Light is OFF
remote.setCommand(turnOff);
remote.pressButton(); // Light is OFF
remote.pressUndo(); // Light is ON
// Macro Command (Composite)
class MacroCommand implements Command {
private commands: Command[] = [];
add(command: Command): void {
this.commands.push(command);
}
execute(): void {
this.commands.forEach(command => command.execute());
}
undo(): void {
// Undo in reverse order
for (let i = this.commands.length - 1; i >= 0; i--) {
if (this.commands[i].undo) {
this.commands[i].undo();
}
}
}
}
// Usage
const macro = new MacroCommand();
macro.add(turnOn);
macro.add(new TurnOnLightCommand(new Light())); // Another light
remote.setCommand(macro);
remote.pressButton(); // Both lights turn on
remote.pressUndo(); // Both lights turn off
// Generic command interface with typed receiver
interface Command<T> {
execute(receiver: T): void;
undo?(receiver: T): void;
}
// Receiver
class BankAccount {
private balance: number = 0;
deposit(amount: number): void {
this.balance += amount;
console.log(`Deposited ${amount}, balance is now ${this.balance}`);
}
withdraw(amount: number): boolean {
if (this.balance >= amount) {
this.balance -= amount;
console.log(`Withdrew ${amount}, balance is now ${this.balance}`);
return true;
}
console.log(`Failed to withdraw ${amount}, balance is ${this.balance}`);
return false;
}
getBalance(): number {
return this.balance;
}
}
// Concrete Command
class DepositCommand implements Command<BankAccount> {
constructor(private amount: number) {}
execute(account: BankAccount): void {
account.deposit(this.amount);
}
undo(account: BankAccount): void {
account.withdraw(this.amount);
}
}
// Concrete Command
class WithdrawCommand implements Command<BankAccount> {
private succeeded: boolean = false;
constructor(private amount: number) {}
execute(account: BankAccount): void {
this.succeeded = account.withdraw(this.amount);
}
undo(account: BankAccount): void {
if (this.succeeded) {
account.deposit(this.amount);
}
}
}
// Invoker
class BankAccountManager {
private commands: Command<BankAccount>[] = [];
private account: BankAccount;
constructor(account: BankAccount) {
this.account = account;
}
executeCommand(command: Command<BankAccount>): void {
command.execute(this.account);
this.commands.push(command);
}
undoLastCommand(): void {
const command = this.commands.pop();
if (command && command.undo) {
command.undo(this.account);
}
}
}
// Usage
const account = new BankAccount();
const manager = new BankAccountManager(account);
manager.executeCommand(new DepositCommand(100)); // Deposited 100, balance is now 100
manager.executeCommand(new WithdrawCommand(50)); // Withdrew 50, balance is now 50
manager.executeCommand(new WithdrawCommand(200)); // Failed to withdraw 200, balance is 50
manager.undoLastCommand(); // Nothing happens (last withdraw failed)
manager.undoLastCommand(); // Deposited 50, balance is now 100
Chain of Responsibility
“Pass a request down a chain until someone handles it.”
Imagine a customer complaint. First, it goes to the customer service agent, then to a manager, and finally to the CEO. Each one checks if they can handle it. If not, they forward it to the next level.
The Chain of Responsibility pattern is a behavioral design pattern that lets you pass requests along a chain of handlers, allowing multiple objects a chance to handle the request without knowing who will handle it.
This is especially useful when:
Key Characteristics
Potential Pitfalls
Real-World Use Cases
When to Use
When to Avoid
// Handler interface
interface Handler {
setNext(handler: Handler): Handler;
handle(request: string): string | null;
}
// Abstract handler implementing the chain mechanism
abstract class AbstractHandler implements Handler {
private nextHandler: Handler | null = null;
public setNext(handler: Handler): Handler {
this.nextHandler = handler;
return handler;
}
public handle(request: string): string | null {
if (this.nextHandler) {
return this.nextHandler.handle(request);
}
return null;
}
}
// Concrete handlers
class MonkeyHandler extends AbstractHandler {
public handle(request: string): string | null {
if (request === 'Banana') {
return `Monkey: I'll eat the ${request}.`;
}
return super.handle(request);
}
}
class SquirrelHandler extends AbstractHandler {
public handle(request: string): string | null {
if (request === 'Nut') {
return `Squirrel: I'll eat the ${request}.`;
}
return super.handle(request);
}
}
class DogHandler extends AbstractHandler {
public handle(request: string): string | null {
if (request === 'MeatBall') {
return `Dog: I'll eat the ${request}.`;
}
return super.handle(request);
}
}
// Client code
function clientCode(handler: Handler) {
const foods = ['Nut', 'Banana', 'Cup of coffee', 'MeatBall'];
for (const food of foods) {
console.log(`Client: Who wants a ${food}?`);
const result = handler.handle(food);
if (result) {
console.log(` ${result}`);
} else {
console.log(` ${food} was left untouched.`);
}
}
}
// Building the chain
const monkey = new MonkeyHandler();
const squirrel = new SquirrelHandler();
const dog = new DogHandler();
monkey.setNext(squirrel).setNext(dog);
// Start the chain
console.log('Chain: Monkey > Squirrel > Dog\n');
clientCode(monkey);
console.log('\nSubchain: Squirrel > Dog\n');
clientCode(squirrel);
interface Middleware {
next(middleware: Middleware): Middleware;
handle(request: any): any;
}
abstract class AbstractMiddleware implements Middleware {
private nextMiddleware: Middleware | null = null;
public next(middleware: Middleware): Middleware {
this.nextMiddleware = middleware;
return middleware;
}
public handle(request: any): any {
if (this.nextMiddleware) {
return this.nextMiddleware.handle(request);
}
return request;
}
}
class AuthMiddleware extends AbstractMiddleware {
public handle(request: any): any {
console.log('AuthMiddleware: Checking authentication');
if (!request.user) {
throw new Error('Unauthorized');
}
return super.handle(request);
}
}
class LoggingMiddleware extends AbstractMiddleware {
public handle(request: any): any {
console.log('LoggingMiddleware: Logging request');
console.log(request);
return super.handle(request);
}
}
class ValidationMiddleware extends AbstractMiddleware {
public handle(request: any): any {
console.log('ValidationMiddleware: Validating data');
if (!request.data) {
throw new Error('Invalid data');
}
return super.handle(request);
}
}
// Building the middleware chain
const auth = new AuthMiddleware();
const logging = new LoggingMiddleware();
const validation = new ValidationMiddleware();
auth.next(logging).next(validation);
// Using the middleware
try {
const request = { user: 'admin', data: 'secret
Mediator
“Centralize complex communication.”
The Mediator pattern is a behavioral design pattern that reduces coupling between components by having them communicate indirectly through a central mediator object instead of directly with each other.
Key Characteristics
Potential Pitfalls
Real-World Use Cases
When to Use
When to Avoid
// Mediator Interface
interface Mediator {
notify(sender: object, event: string, payload?: any): void;
}
// Concrete Mediator
class ConcreteMediator implements Mediator {
private component1: Component1;
private component2: Component2;
constructor(c1: Component1, c2: Component2) {
this.component1 = c1;
this.component1.setMediator(this);
this.component2 = c2;
this.component2.setMediator(this);
}
public notify(sender: object, event: string, payload?: any): void {
if (event === 'A') {
console.log('Mediator reacts on A and triggers following operations:');
this.component2.doC();
}
if (event === 'D') {
console.log('Mediator reacts on D and triggers following operations:');
this.component1.doB();
this.component2.doC();
}
}
}
// Base Component
class BaseComponent {
protected mediator: Mediator | null = null;
public setMediator(mediator: Mediator): void {
this.mediator = mediator;
}
}
// Concrete Component 1
class Component1 extends BaseComponent {
public doA(): void {
console.log('Component 1 does A.');
this.mediator?.notify(this, 'A');
}
public doB(): void {
console.log('Component 1 does B.');
this.mediator?.notify(this, 'B');
}
}
// Concrete Component 2
class Component2 extends BaseComponent {
public doC(): void {
console.log('Component 2 does C.');
this.mediator?.notify(this, 'C');
}
public doD(): void {
console.log('Component 2 does D.');
this.mediator?.notify(this, 'D');
}
}
// Usage
const c1 = new Component1();
const c2 = new Component2();
const mediator = new ConcreteMediator(c1, c2);
console.log('Client triggers operation A.');
c1.doA();
console.log('\nClient triggers operation D.');
c2.doD();
// Mediator Interface
interface ChatRoomMediator {
showMessage(user: User, message: string): void;
}
// Concrete Mediator
class ChatRoom implements ChatRoomMediator {
showMessage(user: User, message: string): void {
const time = new Date().toLocaleTimeString();
const sender = user.getName();
console.log(`${time} [${sender}]: ${message}`);
}
}
// Colleague
class User {
constructor(
private name: string,
private chatMediator: ChatRoomMediator
) {}
getName(): string {
return this.name;
}
send(message: string): void {
this.chatMediator.showMessage(this, message);
}
}
// Usage
const chatRoom = new ChatRoom();
const user1 = new User('John Doe', chatRoom);
const user2 = new User('Jane Doe', chatRoom);
user1.send('Hi there!');
user2.send('Hey!');
State
"Objects that change their behavior when their state changes."
Think of a media player with states like:
Each state has different behavior for a button press. For example:
The State Pattern is a behavioral design pattern that allows an object to change its behavior when its internal state changes. It appears as if the object changed its class. Example: A traffic light switching between red, yellow, and green.
Key Characteristics
Potential Pitfalls
Real-World Use Cases
When to Use
When to Avoid
// State interface
interface VendingMachineState {
insertMoney(amount: number): void;
selectItem(item: string): void;
dispenseItem(): void;
returnMoney(): void;
}
// Concrete States
class NoMoneyState implements VendingMachineState {
constructor(private vendingMachine: VendingMachine) {}
insertMoney(amount: number): void {
console.log(`Inserted $${amount}`);
this.vendingMachine.setCurrentAmount(amount);
this.vendingMachine.setState(new HasMoneyState(this.vendingMachine));
}
selectItem(): void {
console.log("Please insert money first");
}
dispenseItem(): void {
console.log("Please insert money first");
}
returnMoney(): void {
console.log("No money to return");
}
}
class HasMoneyState implements VendingMachineState {
constructor(private vendingMachine: VendingMachine) {}
insertMoney(amount: number): void {
console.log(`Added $${amount} to current amount`);
this.vendingMachine.addToCurrentAmount(amount);
}
selectItem(item: string): void {
const itemPrice = this.vendingMachine.getItemPrice(item);
if (itemPrice > this.vendingMachine.getCurrentAmount()) {
console.log("Not enough money");
return;
}
console.log(`Selected ${item}, price: $${itemPrice}`);
this.vendingMachine.setSelectedItem(item);
this.vendingMachine.setState(new ItemSelectedState(this.vendingMachine));
}
dispenseItem(): void {
console.log("Please select an item first");
}
returnMoney(): void {
console.log(`Returning $${this.vendingMachine.getCurrentAmount()}`);
this.vendingMachine.setCurrentAmount(0);
this.vendingMachine.setState(new NoMoneyState(this.vendingMachine));
}
}
class ItemSelectedState implements VendingMachineState {
constructor(private vendingMachine: VendingMachine) {}
insertMoney(): void {
console.log("Item already selected");
}
selectItem(): void {
console.log("Item already selected");
}
dispenseItem(): void {
const item = this.vendingMachine.getSelectedItem();
const price = this.vendingMachine.getItemPrice(item);
const change = this.vendingMachine.getCurrentAmount() - price;
console.log(`Dispensing ${item}`);
if (change > 0) {
console.log(`Returning change: $${change}`);
}
this.vendingMachine.setCurrentAmount(0);
this.vendingMachine.setSelectedItem("");
this.vendingMachine.setState(new NoMoneyState(this.vendingMachine));
}
returnMoney(): void {
console.log(`Cancelling selection, returning $${this.vendingMachine.getCurrentAmount()}`);
this.vendingMachine.setCurrentAmount(0);
this.vendingMachine.setSelectedItem("");
this.vendingMachine.setState(new NoMoneyState(this.vendingMachine));
}
}
// Context
class VendingMachine {
private currentState: VendingMachineState;
private currentAmount: number = 0;
private selectedItem: string = "";
private itemPrices: Record<string, number> = {
"Soda": 1.50,
"Chips": 1.00,
"Candy": 0.75
};
constructor() {
this.currentState = new NoMoneyState(this);
}
setState(state: VendingMachineState): void {
this.currentState = state;
}
// State delegation methods
insertMoney(amount: number): void {
this.currentState.insertMoney(amount);
}
selectItem(item: string): void {
this.currentState.selectItem(item);
}
dispenseItem(): void {
this.currentState.dispenseItem();
}
returnMoney(): void {
this.currentState.returnMoney();
}
// Getters and setters
getCurrentAmount(): number {
return this.currentAmount;
}
setCurrentAmount(amount: number): void {
this.currentAmount = amount;
}
addToCurrentAmount(amount: number): void {
this.currentAmount += amount;
}
getSelectedItem(): string {
return this.selectedItem;
}
setSelectedItem(item: string): void {
this.selectedItem = item;
}
getItemPrice(item: string): number {
return this.itemPrices[item] || 0;
}
}
// Usage
const vendingMachine = new VendingMachine();
vendingMachine.selectItem("Soda"); // "Please insert money first"
vendingMachine.insertMoney(2.00); // "Inserted $2"
vendingMachine.selectItem("Soda"); // "Selected Soda, price: $1.5"
vendingMachine.dispenseItem(); // "Dispensing Soda", "Returning change: $0.5"
Template Method
"Define the skeleton of an algorithm, but let subclasses fill in the steps."
Suppose you're building a data parser. All parsers do the following:
The structure is the same, but steps like parsing/validation vary depending on file type (e.g., CSV, JSON).
The Template Pattern is a behavioral design pattern that defines the skeleton of an algorithm in a method, deferring some steps to subclasses. It lets subclasses redefine certain steps of an algorithm without changing its structure.
Example: A data exporter that defines general steps, but customizes output format.
Key Characteristics
Potential Pitfalls
Real-World Use Cases
When to Use
When to Avoid
// Abstract base class defining the template method
abstract class DataProcessor {
// The template method defining the algorithm skeleton
public processData(): void {
this.readData();
this.transformData();
if (this.shouldFilter()) {
this.filterData();
}
this.writeData();
}
// Abstract steps to be implemented by subclasses
protected abstract readData(): void;
protected abstract transformData(): void;
protected abstract writeData(): void;
// Hook method - optional step with default implementation
protected filterData(): void {
console.log("Default filtering applied");
}
// Hook method - can be overridden to change behavior
protected shouldFilter(): boolean {
return true;
}
}
// Concrete implementation for CSV processing
class CsvDataProcessor extends DataProcessor {
protected readData(): void {
console.log("Reading data from CSV file");
}
protected transformData(): void {
console.log("Transforming CSV data");
}
protected writeData(): void {
console.log("Writing processed CSV data to destination");
}
protected shouldFilter(): boolean {
return false; // CSV processing doesn't need filtering
}
}
// Concrete implementation for JSON processing
class JsonDataProcessor extends DataProcessor {
protected readData(): void {
console.log("Reading data from JSON file");
}
protected transformData(): void {
console.log("Transforming JSON data");
}
protected writeData(): void {
console.log("Writing processed JSON data to destination");
}
protected filterData(): void {
console.log("Applying custom JSON filtering");
}
}
// Usage
const csvProcessor = new CsvDataProcessor();
csvProcessor.processData();
const jsonProcessor = new JsonDataProcessor();
jsonProcessor.processData();
abstract class Beverage {
// The template method - final to prevent overriding
public final prepareBeverage(): void {
this.boilWater();
this.brew();
this.pourInCup();
if (this.customerWantsCondiments()) {
this.addCondiments();
}
}
// Abstract methods to be implemented by subclasses
protected abstract brew(): void;
protected abstract addCondiments(): void;
// Common operations with default implementations
protected boilWater(): void {
console.log("Boiling water");
}
protected pourInCup(): void {
console.log("Pouring into cup");
}
// Hook method - can be overridden
protected customerWantsCondiments(): boolean {
return true;
}
}
class Coffee extends Beverage {
protected brew(): void {
console.log("Dripping coffee through filter");
}
protected addCondiments(): void {
console.log("Adding sugar and milk");
}
protected customerWantsCondiments(): boolean {
// Could be determined by user input in a real app
return confirm("Would you like milk and sugar?");
}
}
class Tea extends Beverage {
protected brew(): void {
console.log("Steeping the tea");
}
protected addCondiments(): void {
console.log("Adding lemon");
}
}
// Usage
const coffee = new Coffee();
coffee.prepareBeverage();
const tea = new Tea();
tea.prepareBeverage();
Visitor
"Separate an algorithm from the object structure it works on."
Imagine you have a complex object structure (like a tree of nodes), and you want to perform various unrelated operations (e.g., rendering, exporting, analytics) on these objects. Adding all those operations directly into the classes clutters them and violates the Single Responsibility Principle.
The Visitor Pattern allows you to add these operations separately, without modifying the object structure.
The Visitor Pattern is a behavioral design pattern that lets you separate algorithms from the objects on which they operate. It allows adding new operations to existing object structures without modifying them.
Example: A tax calculator that visits different types of income records.
Key Characteristics
Potential Pitfalls
Real-World Use Cases
When to Use
When to Avoid
// Element interface
interface Shape {
accept(visitor: ShapeVisitor): void;
}
// Concrete Elements
class Circle implements Shape {
constructor(public radius: number) {}
accept(visitor: ShapeVisitor): void {
visitor.visitCircle(this);
}
}
class Square implements Shape {
constructor(public side: number) {}
accept(visitor: ShapeVisitor): void {
visitor.visitSquare(this);
}
}
// Visitor interface
interface ShapeVisitor {
visitCircle(circle: Circle): void;
visitSquare(square: Square): void;
}
// Concrete Visitors
class AreaCalculator implements ShapeVisitor {
visitCircle(circle: Circle): void {
const area = Math.PI * circle.radius ** 2;
console.log(`Area of circle with radius ${circle.radius}: ${area.toFixed(2)}`);
}
visitSquare(square: Square): void {
const area = square.side ** 2;
console.log(`Area of square with side ${square.side}: ${area}`);
}
}
class PerimeterCalculator implements ShapeVisitor {
visitCircle(circle: Circle): void {
const perimeter = 2 * Math.PI * circle.radius;
console.log(`Perimeter of circle with radius ${circle.radius}: ${perimeter.toFixed(2)}`);
}
visitSquare(square: Square): void {
const perimeter = 4 * square.side;
console.log(`Perimeter of square with side ${square.side}: ${perimeter}`);
}
}
// Usage
const shapes: Shape[] = [
new Circle(5),
new Square(4),
new Circle(3)
];
const areaVisitor = new AreaCalculator();
const perimeterVisitor = new PerimeterCalculator();
console.log('Calculating areas:');
shapes.forEach(shape => shape.accept(areaVisitor));
console.log('\nCalculating perimeters:');
shapes.forEach(shape => shape.accept(perimeterVisitor));
// Composite example
class ShapeGroup implements Shape {
private shapes: Shape[] = [];
add(shape: Shape): void {
this.shapes.push(shape);
}
accept(visitor: ShapeVisitor): void {
visitor.visitShapeGroup(this);
this.shapes.forEach(shape => shape.accept(visitor));
}
}
// Extended visitor interface
interface ShapeVisitor {
visitCircle(circle: Circle): void;
visitSquare(square: Square): void;
visitShapeGroup(group: ShapeGroup): void;
}
// Updated AreaCalculator
class AreaCalculator implements ShapeVisitor {
private totalArea = 0;
visitCircle(circle: Circle): void {
const area = Math.PI * circle.radius ** 2;
this.totalArea += area;
console.log(`Adding circle area: ${area.toFixed(2)}`);
}
visitSquare(square: Square): void {
const area = square.side ** 2;
this.totalArea += area;
console.log(`Adding square area: ${area}`);
}
visitShapeGroup(group: ShapeGroup): void {
console.log('Entering shape group');
}
getTotalArea(): number {
return this.totalArea;
}
}
// Usage with composite
const group = new ShapeGroup();
group.add(new Circle(5));
group.add(new Square(4));
const complexShape = new ShapeGroup();
complexShape.add(new Circle(3));
complexShape.add(group);
const areaVisitor = new AreaCalculator();
complexShape.accept(areaVisitor);
console.log(`Total area: ${areaVisitor.getTotalArea().toFixed(2)}`);
Memento
"Save and restore the state of an object."
Think of an "undo" button in a text editor. Every time you type something, it saves a snapshot. When you click "undo", it restores the previous state without needing to know how the text was managed internally.. Example: Save/load feature in a game.
The Memento Pattern is a behavioral design pattern that lets you capture and store the current state of an object so it can be restored later, without exposing its internal structure.
Key Characteristics
Potential Pitfalls
Real-World Use Cases
When to Use
When to Avoid
// Memento class - stores the state of the Originator
class Memento {
constructor(private state: string) {}
getState(): string {
return this.state;
}
}
// Originator class - creates and restores mementos
class Originator {
private state: string;
constructor(state: string) {
this.state = state;
console.log(`Originator: Initial state is ${state}`);
}
doSomething(): void {
console.log("Originator: Doing something important...");
this.state = this.generateRandomString(30);
console.log(`Originator: State changed to ${this.state}`);
}
private generateRandomString(length: number): string {
const charSet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
return Array.from({length}, () =>
charSet[Math.floor(Math.random() * charSet.length)]
).join('');
}
save(): Memento {
return new Memento(this.state);
}
restore(memento: Memento): void {
this.state = memento.getState();
console.log(`Originator: State restored to ${this.state}`);
}
}
// Caretaker class - manages mementos
class Caretaker {
private mementos: Memento[] = [];
private originator: Originator;
constructor(originator: Originator) {
this.originator = originator;
}
backup(): void {
console.log("\nCaretaker: Saving Originator's state...");
this.mementos.push(this.originator.save());
}
undo(): void {
if (!this.mementos.length) return;
const memento = this.mementos.pop();
console.log(`Caretaker: Restoring state to: ${memento?.getState()}`);
this.originator.restore(memento!);
}
showHistory(): void {
console.log("Caretaker: Here's the list of mementos:");
for (const memento of this.mementos) {
console.log(memento.getState());
}
}
}
// Client code
const originator = new Originator("Initial state");
const caretaker = new Caretaker(originator);
caretaker.backup();
originator.doSomething();
caretaker.backup();
originator.doSomething();
caretaker.backup();
originator.doSomething();
console.log("");
caretaker.showHistory();
console.log("\nClient: Now, let's rollback!\n");
caretaker.undo();
caretaker.undo();
Interpreter
"Define your own language or grammar and interpret it."
Used when building simple scripting or command languages. Example: A calculator or query engine parsing custom user input.
The Interpreter Pattern is a behavioral design pattern used to define a grammar for a language and provide an interpreter to interpret sentences in that language. It’s commonly used for parsing expressions, rules, or custom scripting languages.
Key Characteristics
Potential Pitfalls
Real-World Use Cases
When to Use
When to Avoid
// Abstract Expression
interface Expression {
interpret(): number;
}
// Terminal Expression
class NumberExpression implements Expression {
constructor(private value: number) {}
interpret(): number {
return this.value;
}
}
// Non-terminal Expression
class AddExpression implements Expression {
constructor(private left: Expression, private right: Expression) {}
interpret(): number {
return this.left.interpret() + this.right.interpret();
}
}
class SubtractExpression implements Expression {
constructor(private left: Expression, private right: Expression) {}
interpret(): number {
return this.left.interpret() - this.right.interpret();
}
}
class MultiplyExpression implements Expression {
constructor(private left: Expression, private right: Expression) {}
interpret(): number {
return this.left.interpret() * this.right.interpret();
}
}
class DivideExpression implements Expression {
constructor(private left: Expression, private right: Expression) {}
interpret(): number {
return this.left.interpret() / this.right.interpret();
}
}
// Context (optional in simple cases)
class Context {
// Could store variables or other context here
}
// Client code
function parseExpression(tokens: string[]): Expression {
// This is a simplified parser - real implementations would be more complex
let stack: Expression[] = [];
for (let token of tokens) {
switch (token) {
case '+':
const rightAdd = stack.pop()!;
const leftAdd = stack.pop()!;
stack.push(new AddExpression(leftAdd, rightAdd));
break;
case '-':
const rightSub = stack.pop()!;
const leftSub = stack.pop()!;
stack.push(new SubtractExpression(leftSub, rightSub));
break;
case '*':
const rightMul = stack.pop()!;
const leftMul = stack.pop()!;
stack.push(new MultiplyExpression(leftMul, rightMul));
break;
case '/':
const rightDiv = stack.pop()!;
const leftDiv = stack.pop()!;
stack.push(new DivideExpression(leftDiv, rightDiv));
break;
default:
// Assume it's a number
stack.push(new NumberExpression(parseFloat(token)));
}
}
return stack.pop()!;
}
// Example usage
const expression = "3 4 2 * 1 5 - 2 3 ^ ^ / +";
const tokens = expression.split(' ').filter(token => token.trim() !== '');
const ast = parseExpression(tokens);
console.log(`Result of "${expression}":`, ast.interpret()); // Result: 3.0001220703125
// Abstract Expression
interface BooleanExpression {
interpret(context: VariableContext): boolean;
}
// Context
class VariableContext {
private variables: Record<string, boolean> = {};
setVariable(name: string, value: boolean): void {
this.variables[name] = value;
}
getVariable(name: string): boolean {
return this.variables[name];
}
}
// Terminal Expressions
class VariableExpression implements BooleanExpression {
constructor(private name: string) {}
interpret(context: VariableContext): boolean {
return context.getVariable(this.name);
}
}
class ConstantExpression implements BooleanExpression {
constructor(private value: boolean) {}
interpret(_context: VariableContext): boolean {
return this.value;
}
}
// Non-terminal Expressions
class AndExpression implements BooleanExpression {
constructor(
private left: BooleanExpression,
private right: BooleanExpression
) {}
interpret(context: VariableContext): boolean {
return this.left.interpret(context) && this.right.interpret(context);
}
}
class OrExpression implements BooleanExpression {
constructor(
private left: BooleanExpression,
private right: BooleanExpression
) {}
interpret(context: VariableContext): boolean {
return this.left.interpret(context) || this.right.interpret(context);
}
}
class NotExpression implements BooleanExpression {
constructor(private expression: BooleanExpression) {}
interpret(context: VariableContext): boolean {
return !this.expression.interpret(context);
}
}
// Client code
function parseBooleanExpression(tokens: string[]): BooleanExpression {
// Simplified parser - assumes properly formatted input
const stack: BooleanExpression[] = [];
for (const token of tokens) {
switch (token.toLowerCase()) {
case 'and':
const rightAnd = stack.pop()!;
const leftAnd = stack.pop()!;
stack.push(new AndExpression(leftAnd, rightAnd));
break;
case 'or':
const rightOr = stack.pop()!;
const leftOr = stack.pop()!;
stack.push(new OrExpression(leftOr, rightOr));
break;
case 'not':
const expr = stack.pop()!;
stack.push(new NotExpression(expr));
break;
case 'true':
stack.push(new ConstantExpression(true));
break;
case 'false':
stack.push(new ConstantExpression(false));
break;
default:
// Assume it's a variable
stack.push(new VariableExpression(token));
}
}
return stack.pop()!;
}
// Example usage
const context = new VariableContext();
context.setVariable('x', true);
context.setVariable('y', false);
context.setVariable('z', true);
const expression = "x y and z or not";
const tokens = expression.split(' ').filter(token => token.trim() !== '').reverse();
const ast = parseBooleanExpression(tokens);
console.log(`Result of "${expression}":`, ast.interpret(context)); // Result: false
Modern & Concurrency Patterns – Taming the Async Beast
“Imagine a kitchen with many cooks, timers, and waiters – who does what, and when?”
As software becomes more concurrent and event-driven, these patterns help you manage complexity in asynchronous or multi-threaded environments.
What it solves:
Keeps your app from freezing while waiting for I/O, heavy tasks, or background operations.
Real-world software context:
Thread Pool
"Reuse a fixed set of threads instead of creating new ones for every task."
The Thread Pool pattern is a concurrency pattern that manages a pool of worker threads to execute tasks efficiently, avoiding the overhead of thread creation and destruction for each task.
Key Characteristics
Potential Pitfalls
Real-World Use Cases
When to Use
When to Avoid
TypeScript/Node.js uses worker threads for CPU-intensive tasks since JavaScript is single-threaded by nature.
// In Node.js: Use a worker pool with 'worker_threads' or external libs like Piscina
import { Worker, isMainThread, parentPort, workerData } from 'worker_threads';
import { cpus } from 'os';
class ThreadPool {
private taskQueue: Array<{ task: any; resolve: (value: any) => void; reject: (reason?: any) => void }>;
private workers: Worker[];
private activeWorkers: number;
private maxThreads: number;
constructor(maxThreads: number = cpus().length) {
this.taskQueue = [];
this.workers = [];
this.activeWorkers = 0;
this.maxThreads = maxThreads;
}
public async execute(task: any): Promise<any> {
return new Promise((resolve, reject) => {
if (this.activeWorkers < this.maxThreads) {
this.runTask(task, resolve, reject);
} else {
this.taskQueue.push({ task, resolve, reject });
}
});
}
private runTask(task: any, resolve: (value: any) => void, reject: (reason?: any) => void) {
this.activeWorkers++;
const worker = new Worker(__filename, {
workerData: task
});
worker.on('message', (result) => {
resolve(result);
this.workerFinished(worker);
});
worker.on('error', (error) => {
reject(error);
this.workerFinished(worker);
});
worker.on('exit', (code) => {
if (code !== 0) {
reject(new Error(`Worker stopped with exit code ${code}`));
}
this.workerFinished(worker);
});
this.workers.push(worker);
}
private workerFinished(worker: Worker) {
this.activeWorkers--;
const index = this.workers.indexOf(worker);
if (index !== -1) {
this.workers.splice(index, 1);
}
if (this.taskQueue.length > 0) {
const nextTask = this.taskQueue.shift();
if (nextTask) {
this.runTask(nextTask.task, nextTask.resolve, nextTask.reject);
}
}
}
public async shutdown() {
await Promise.all(this.workers.map(worker => worker.terminate()));
this.taskQueue = [];
this.workers = [];
this.activeWorkers = 0;
}
}
// Worker thread implementation
if (!isMainThread) {
// Simulate CPU-intensive task
function processTask(data: any): any {
// Example: Fibonacci calculation (CPU-intensive)
function fibonacci(n: number): number {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
if (data.task === 'fibonacci') {
return fibonacci(data.value);
}
// Other task types can be added here
throw new Error('Unknown task type');
}
const result = processTask(workerData);
parentPort?.postMessage(result);
}
// Example usage
async function main() {
const pool = new ThreadPool(4); // Pool with 4 worker threads
try {
// Execute multiple tasks
const results = await Promise.all([
pool.execute({ task: 'fibonacci', value: 40 }),
pool.execute({ task: 'fibonacci', value: 38 }),
pool.execute({ task: 'fibonacci', value: 35 }),
pool.execute({ task: 'fibonacci', value: 30 }),
pool.execute({ task: 'fibonacci', value: 25 }),
]);
console.log('Results:', results);
} finally {
await pool.shutdown();
}
}
if (isMainThread) {
main().catch(console.error);
}
Future / Promise
"A placeholder for a result that hasn’t arrived yet."
The Future/Promise pattern is a fundamental concept in asynchronous programming that represents a value that may not be available yet but will be resolved at some point in the future. Example: Fetching data from an API.
Key Characteristics
When to Use
When to Avoid
// Creating a Promise
const fetchData = (url: string): Promise<string> => {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (url === 'https://api.example.com/data') {
resolve('Data retrieved successfully');
} else {
reject(new Error('Invalid URL'));
}
}, 1000);
});
};
// Using the Promise
fetchData('https://api.example.com/data')
.then((data) => {
console.log(data); // "Data retrieved successfully"
})
.catch((error) => {
console.error(error.message);
});
//Promise Timeout
function withTimeout<T>(promise: Promise<T>, timeoutMs: number): Promise<T> {
return new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => {
reject(new Error('Promise timeout'));
}, timeoutMs);
promise
.then(resolve)
.catch(reject)
.finally(() => clearTimeout(timeoutId));
});
}
//Promise Retry
async function retry<T>(
operation: () => Promise<T>,
maxRetries: number,
delayMs: number
): Promise<T> {
try {
return await operation();
} catch (error) {
if (maxRetries <= 0) throw error;
await new Promise(resolve => setTimeout(resolve, delayMs));
return retry(operation, maxRetries - 1, delayMs);
}
}
//Promise Pool (limiting concurrent operations)
async function promisePool<T>(
tasks: (() => Promise<T>)[],
concurrency: number
): Promise<T[]> {
const results: T[] = [];
const executing: Promise<void>[] = [];
for (const task of tasks) {
const p = task().then(result => {
results.push(result);
executing.splice(executing.indexOf(p), 1);
});
executing.push(p);
if (executing.length >= concurrency) {
await Promise.race(executing);
}
}
await Promise.all(executing);
return results;
}
//async await
async function processUserOrder(userId: number): Promise<OrderStatus> {
try {
const user = await getUser(userId);
const cart = await getCart(user.id);
const order = await createOrder(cart.items);
await sendConfirmationEmail(user.email, order.id);
return 'SUCCESS';
} catch (error) {
console.error('Order processing failed:', error);
return 'FAILED';
}
}