What design patterns do you know, and what are their features?

👨‍💻 Frontend Developer 🟠 May come up 🎚️ Hard
#Theory

Quick Answer

Design patterns are reusable solutions to commonly occurring problems in programming. They are not finished code but rather concepts or templates that can be adapted to a specific task. They are usually divided into three main groups:

  1. Creational: Responsible for flexible object creation.

    • Factory Method: Creating objects through a special function rather than directly.
    • Abstract Factory: Creating families of related objects.
    • Singleton: Ensures that a module has only one instance.
    • Builder: Step-by-step creation of complex objects.
  2. Structural: Define how objects can be combined to form larger structures.

    • Adapter: Allows objects with incompatible interfaces to work together.
    • Decorator: Dynamically adds new functionality to an object.
    • Facade: Provides a simple interface to a complex system.
    • Proxy: Substitutes a real object with another one that controls access to it.
  3. Behavioral: Responsible for effective communication between objects.

    • Strategy: Allows defining a family of algorithms and making them interchangeable.
    • Observer: Defines a one-to-many dependency where, when the state of one object changes, all dependents are notified and updated.
    • Command: Turns a request into a stand-alone object.
    • Iterator: Provides a way to sequentially access elements of composite objects without exposing their internal representation.

Detailed Answer with Examples

Creational Patterns

1. Singleton

Ensures that a module has only one instance and provides a global access point to it. In a functional approach, this is achieved using a closure.

Feature: Useful for managing shared state (e.g., application configuration, logging service).

const AppConfig = (() => {
  let instance;
 
  function createInstance() {
    const config = { theme: 'dark' };
    return {
      getConfig: () => config,
    };
  }
 
  return {
    getInstance: () => {
      if (!instance) {
        instance = createInstance();
      }
      return instance;
    },
  };
})();
 
const config1 = AppConfig.getInstance();
const config2 = AppConfig.getInstance();
 
console.log(config1 === config2); // true

Structural Patterns

2. Decorator

Allows dynamically adding new functionality to objects by wrapping them in decorator functions.

Feature: A flexible alternative to inheritance. Ideal for extending functionality without modifying the original object.

const createCoffee = () => ({
  cost: () => 5,
});
 
// Decorator
const withMilk = (coffee) => ({
  ...coffee,
  cost: () => coffee.cost() + 2,
});
 
// Decorator
const withSugar = (coffee) => ({
  ...coffee,
  cost: () => coffee.cost() + 1,
});
 
let myCoffee = createCoffee();
myCoffee = withMilk(myCoffee);
myCoffee = withSugar(myCoffee);
 
console.log(myCoffee.cost()); // 8 (5 + 2 + 1)

3. Facade

Provides a unified and simplified facade function for interacting with a complex subsystem.

Feature: Hides complexity. For example, you can create a facade for working with browser APIs (DOM, Fetch, LocalStorage).

// Complex subsystem
const domApi = {
  createElement: (tag) => { /* ... */ },
  addStyle: (element, styles) => { /* ... */ },
};
 
const fetchApi = {
  get: (url) => { /* ... */ },
};
 
// Facade
const createAppFacade = () => ({
  fetchAndDisplay: (url, elementId) => {
    const data = fetchApi.get(url);
    const element = domApi.createElement('div');
    // ... display logic
  }
});
 
const app = createAppFacade();
// app.fetchAndDisplay('/api/data', '#root');

Behavioral Patterns

4. Observer

Creates a subscription mechanism that allows some objects (observers) to watch for changes in another object (the subject).

Feature: The foundation of reactive programming. Used in state management (Redux, MobX) and the browser’s event model.

const createNewsPublisher = () => {
  let subscribers = [];
 
  return {
    subscribe: (observer) => {
      subscribers.push(observer);
    },
    unsubscribe: (observer) => {
      subscribers = subscribers.filter(obs => obs !== observer);
    },
    notify: (news) => {
      subscribers.forEach(observer => observer.update(news));
    },
  };
};
 
const createReader = (name) => ({
  update: (news) => {
    console.log(`${name} read the news: ${news}`);
  },
});
 
const publisher = createNewsPublisher();
const reader1 = createReader('Ivan');
const reader2 = createReader('Maria');
 
publisher.subscribe(reader1);
publisher.subscribe(reader2);
 
publisher.notify('A new version of JavaScript has been released!');
// Ivan read the news: A new version of JavaScript has been released!
// Maria read the news: A new version of JavaScript has been released!

5. Strategy

Defines a family of similar algorithms (functions) and allows them to be passed as arguments, making them interchangeable.

Feature: Allows changing logic on the fly. For example, choosing a sorting method or a form validation method.

const validate = (strategy, value) => {
  return strategy(value);
};
 
const isRequired = (value) => !!value;
const isEmail = (value) => /@/.test(value);
 
console.log(validate(isRequired, '')); // false
console.log(validate(isEmail, 'test@test.com')); // true