Learn JavaScript Design Patterns from scratch

JavaScript design patterns are helpful tools that make your code work better. Think of them like building with blocks – there are certain ways to stack the blocks that make your tower stronger and less likely to fall over. Design patterns are like those reliable ways of stacking blocks, but for writing code.

When you first see words like “Singleton” or “Observer Pattern” in JavaScript tutorials, they might seem confusing. But they’re actually just names for different ways to solve common coding problems. Just like you might have different methods for organizing your desk or folding your clothes, programmers have different methods for organizing their code.

The nice thing about design patterns is that lots of programmers have already tested them and found that they work well. Instead of having to figure everything out on your own, you can learn from what others have already discovered.

What Are JavaScript Design Patterns?

Imagine you’re building a house. You wouldn’t start from scratch – you’d use blueprints that architects have refined over many years. Design patterns are like these blueprints for code. They help us solve common programming problems in ways that have been tested and improved by developers around the world.

Design patterns represent standardized solutions to recurring problems in software development. These patterns, developed and refined through years of practical implementation, provide developers with reliable approaches to address common architectural challenges.

Why Should You Learn Design Patterns?

Learning design patterns is like adding tools to your programming toolbox. Each pattern helps solve specific problems:

  • They make your code easier to understand and maintain
  • They help you avoid common programming mistakes
  • They give you a common language to discuss code with other developers

Let’s explore 7 fundamental patterns that will immediately improve your JavaScript code. We’ll use simple, real-world examples that you can start using today.

1. Singleton Pattern

The Singleton pattern ensures a class has only one instance and provides a global point of access to that instance. It is used when exactly one object is needed to coordinate actions across the system.

// Singleton Pattern: Ensures only one instance exists
class Database {
    constructor() {
        if (Database.instance) {
            return Database.instance;
        }
        this.connectionString = "default-connection";
        Database.instance = this;
    }

    connect() {
        return `Connected to: ${this.connectionString}`;
    }
}

// Testing the Singleton
console.log("Creating first instance:");
const db1 = new Database();
console.log(db1.connect());  // Output: Connected to: default-connection

console.log("\nCreating second instance:");
const db2 = new Database();
console.log(db2.connect());  // Output: Connected to: default-connection

// Prove it's the same instance
console.log("\nAre they the same instance?");
console.log(db1 === db2);    // Output: true

2. The Observer Pattern

The Observer pattern defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically. It provides a way to react to events happening in other objects without coupling to their classes.

// Create a simple news service
class NewsService {
    constructor() {
        // List of subscribers
        this.subscribers = [];
    }

    // Add a new subscriber
    addSubscriber(subscriber) {
        this.subscribers.push(subscriber);
        console.log('New subscriber added!');
    }

    // Send news to all subscribers
    sendNews(news) {
        // For each subscriber, send them the news
        this.subscribers.forEach(subscriber => {
            subscriber(news);
        });
    }
}

// Create our news service
const newsService = new NewsService();

// Add some subscribers
newsService.addSubscriber(news => {
    console.log('John received:', news);
});

newsService.addSubscriber(news => {
    console.log('Jane received:', news);
});

// Send some news
newsService.sendNews('JavaScript is fun!');
// Output:
// John received: JavaScript is fun!
// Jane received: JavaScript is fun!

3. The Factory Pattern:

The Factory pattern provides an interface for creating objects but allows subclasses to alter the type of objects that will be created. It encapsulates object creation logic and makes it independent from the client code.

class NewsAgency {
    constructor() {
        this.subscribers = [];
    }

    subscribe(subscriber) {
        this.subscribers.push(subscriber);
    }

    notify(news) {
        this.subscribers.forEach(subscriber => subscriber(news));
    }
}

const agency = new NewsAgency();

// Add two subscribers
agency.subscribe(news => console.log(`John got news: ${news}`));
agency.subscribe(news => console.log(`Jane got news: ${news}`));

// Send out news
agency.notify('JavaScript is awesome!');

// Output:
// John got news: JavaScript is awesome!
// Jane got news: JavaScript is awesome!

4. Module Pattern

The Module pattern encapsulates ‘private’ information, state, and organization using closures. It provides a way of wrapping a mix of public and private methods and variables, protecting pieces from leaking into the global scope.

const bankAccount = (function() {
    let balance = 0;  // Private - can't be accessed directly
    
    return {
        deposit: amount => {
            balance += amount;
            return balance;
        },
        getBalance: () => balance
    };
})();

console.log(bankAccount.balance);     // Output: undefined (private!)
console.log(bankAccount.deposit(100)); // Output: 100
console.log(bankAccount.getBalance()); // Output: 100

5. Decorator Pattern

The Decorator pattern attaches additional responsibilities to an object dynamically. It provides a flexible alternative to subclassing for extending functionality.

class Coffee {
    cost() { return 5; }
    description() { return 'Coffee'; }
}

function withMilk(coffee) {
    const cost = coffee.cost();
    coffee.cost = () => cost + 2;
    coffee.description = () => coffee.description() + ' with milk';
    return coffee;
}

let myCoffee = new Coffee();
console.log(myCoffee.cost());        // Output: 5
console.log(myCoffee.description()); // Output: 'Coffee'

myCoffee = withMilk(myCoffee);
console.log(myCoffee.cost());        // Output: 7
console.log(myCoffee.description()); // Output: 'Coffee with milk'

6. Strategy Pattern

The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. It lets the algorithm vary independently from clients that use it.

const paymentMethods = {
    creditCard: amount => `Paid $${amount} with credit card`,
    paypal: amount => `Paid $${amount} with PayPal`,
    cash: amount => `Paid $${amount} in cash`
};

class Payment {
    pay(method, amount) {
        return paymentMethods[method](amount);
    }
}

const payment = new Payment();
console.log(payment.pay('creditCard', 50)); // Output: 'Paid $50 with credit card'
console.log(payment.pay('paypal', 30));     // Output: 'Paid $30 with PayPal'

7. Command Pattern

The Command pattern encapsulates a request as an object, thereby letting you parameterize clients with different requests, queue or log requests, and support undoable operations.

class Light {
    turnOn() { return 'Light is on'; }
    turnOff() { return 'Light is off'; }
}

class RemoteControl {
    submit(command) {
        return command.execute();
    }
}

// Commands
const lightOn = light => ({
    execute: () => light.turnOn()
});

const lightOff = light => ({
    execute: () => light.turnOff()
});

const light = new Light();
const remote = new RemoteControl();

console.log(remote.submit(lightOn(light)));  // Output: 'Light is on'
console.log(remote.submit(lightOff(light))); // Output: 'Light is off'

Real-World Example: A Simple Todo List

Let’s combine these patterns to create a simple todo list:

// Todo List using multiple patterns
const todoList = (function() {
    // Private array to store todos
    let todos = [];

    // List of functions to call when todos change
    let observers = [];

    return {
        // Add a new todo
        addTodo: function(text) {
            const todo = {
                id: Date.now(),
                text: text,
                completed: false
            };

            todos.push(todo);

            // Notify all observers that todos changed
            observers.forEach(observer => observer(todos));
        },

        // Toggle a todo's completion
        toggleTodo: function(id) {
            todos = todos.map(todo => {
                if (todo.id === id) {
                    todo.completed = !todo.completed;
                }
                return todo;
            });

            // Notify all observers that todos changed
            observers.forEach(observer => observer(todos));
        },

        // Add an observer function
        onChange: function(observerFunction) {
            observers.push(observerFunction);
        }
    };
})();

// Using our todo list
todoList.onChange(todos => {
    console.log('Todos updated:', todos);
});

todoList.addTodo('Learn JavaScript');
todoList.addTodo('Practice design patterns');

This example shows how patterns work together:

  • Module Pattern: Keeps our todos private
  • Observer Pattern: Updates the display when todos change
  • Factory-like creation: Creates structured todo objects

When to Use Each Pattern

  • Singleton: When you need exactly one instance shared everywhere (app settings, database connections)
  • Factory: When object creation is complex or needs to be centralized
  • Observer: When changes in one object should automatically notify other objects
  • Module: When you need to organize code with private and public parts
  • Decorator: When you need to add features to objects dynamically
  • Strategy: When you have a family of similar algorithms to choose from
  • Command: When you want to decouple the execution of an action from its implementation

Conclusion

Design patterns are powerful tools that can help you write better JavaScript code. We’ve covered three fundamental patterns – Module, Observer, and Factory – with simple examples you can start using today.

The key is to practice. Start small:

  1. Try using the Module pattern to organize your next project
  2. Add an Observer pattern when you need to keep different parts of your code in sync
  3. Use the Factory pattern when you find yourself creating similar objects repeatedly

Remember, every experienced developer started where you are now. Take these patterns, experiment with them, and make them your own. The more you practice, the more natural they’ll become.

Happy coding!

Previous Article

Learn JavaScript Inheritance from Basics

Next Article

What are JavaScript Object Methods and Properties

Write a Comment

Leave a Comment

Your email address will not be published. Required fields are marked *

Subscribe to our Newsletter

Subscribe to our email newsletter to get the latest posts delivered right to your email.
Pure inspiration, zero spam ✨