Fundamentals 35 min read

JavaScript Design Patterns: Factory Method, Abstract Factory, Singleton, Builder, Prototype and More

This article introduces JavaScript design patterns—covering creational, structural, and behavioral categories—and provides clear explanations and complete code examples for patterns such as Factory Method, Abstract Factory, Singleton, Builder, Prototype, Adapter, Decorator, Proxy, Facade, Bridge, Composite, Flyweight, Strategy, Template Method, Observer, Iterator, Chain of Responsibility, Command, Memento, State, Visitor, Mediator and Interpreter, helping developers write cleaner, more maintainable code.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
JavaScript Design Patterns: Factory Method, Abstract Factory, Singleton, Builder, Prototype and More

The article introduces the concept of design patterns in JavaScript, encouraging developers who feel lost in their code to explore patterns that can make code cleaner and more maintainable.

Design patterns are divided into three major categories: Creational, Structural, and Behavioral.

Creational patterns covered include Factory Method, Abstract Factory, Singleton, Builder, and Prototype.

Factory Method Pattern defines an interface for creating objects while allowing subclasses to decide which class to instantiate. The example defines an abstract Animal class, concrete Dog and Cat subclasses, and an AnimalFactory that creates instances based on a string.

class Animal {
  // 定义抽象方法 speak,该方法必须在子类中被实现
  speak() {
    throw new Error('This method must be implemented.');
  }
}

class Dog extends Animal {
  speak() { return 'Woof!'; }
}

class Cat extends Animal {
  speak() { return 'Meow!'; }
}

class AnimalFactory {
  createAnimal(animalType) {
    switch(animalType) {
      case 'dog':
        return new Dog();
      case 'cat':
        return new Cat();
      default:
        throw new Error(`Invalid animal type: ${animalType}`);
    }
  }
}

const animalFactory = new AnimalFactory();
const dog = animalFactory.createAnimal('dog');
console.log(dog.speak()); // Output: Woof!
const cat = animalFactory.createAnimal('cat');
console.log(cat.speak()); // Output: Meow!

Abstract Factory Pattern provides a way to encapsulate a group of related factories without specifying concrete classes. The example creates abstract product interfaces AnimalFood and AnimalToy , concrete products like HighQualityDogFood , and abstract factories AnimalProductsAbstractFactory with concrete factories for high‑quality dog products and cheap cat products.

class AnimalFood {
  provide() { throw new Error('This method must be implemented.'); }
}

class AnimalToy {
  provide() { throw new Error('This method must be implemented.'); }
}

class HighQualityDogFood extends AnimalFood {
  provide() { return 'High quality dog food'; }
}

class HighQualityDogToy extends AnimalToy {
  provide() { return 'High quality dog toy'; }
}

class CheapCatFood extends AnimalFood {
  provide() { return 'Cheap cat food'; }
}

class CheapCatToy extends AnimalToy {
  provide() { return 'Cheap cat toy'; }
}

class AnimalProductsAbstractFactory {
  createFood() { throw new Error('This method must be implemented.'); }
  createToy() { throw new Error('This method must be implemented.'); }
}

class HighQualityAnimalProductsFactory extends AnimalProductsAbstractFactory {
  createFood() { return new HighQualityDogFood(); }
  createToy() { return new HighQualityDogToy(); }
}

class CheapAnimalProductsFactory extends AnimalProductsAbstractFactory {
  createFood() { return new CheapCatFood(); }
  createToy() { return new CheapCatToy(); }
}

const highQualityFactory = new HighQualityAnimalProductsFactory();
console.log(highQualityFactory.createFood().provide()); // High quality dog food
console.log(highQualityFactory.createToy().provide());   // High quality dog toy

const cheapFactory = new CheapAnimalProductsFactory();
console.log(cheapFactory.createFood().provide()); // Cheap cat food
console.log(cheapFactory.createToy().provide());   // Cheap cat toy

Singleton Pattern ensures a class has only one instance and provides a global access point. The example shows a Logger class that returns the same instance on each construction and records log messages.

class Logger {
  constructor() {
    if (!Logger.instance) {
      this.logs = [];
      Logger.instance = this;
    }
    return Logger.instance;
  }
  log(message) {
    this.logs.push(message);
    console.log(`Logger: ${message}`);
  }
  printLogCount() {
    console.log(`Number of logs: ${this.logs.length}`);
  }
}

const logger = new Logger();
Object.freeze(logger);
logger.log('First message'); // Logger: First message
logger.printLogCount();      // Number of logs: 1
const anotherLogger = new Logger(); // returns existing instance
anotherLogger.log('Second message'); // Logger: Second message
anotherLogger.printLogCount();      // Number of logs: 2

Builder Pattern separates the construction of a complex object from its representation. The example builds a Sandwich step by step using SandwichBuilder and a director SandwichMaker .

class Sandwich {
  constructor() { this.ingredients = []; }
  addIngredient(ingredient) { this.ingredients.push(ingredient); }
  toString() { return this.ingredients.join(', '); }
}

class SandwichBuilder {
  constructor() { this.sandwich = new Sandwich(); }
  reset() { this.sandwich = new Sandwich(); }
  putMeat(meat) { this.sandwich.addIngredient(meat); }
  putCheese(cheese) { this.sandwich.addIngredient(cheese); }
  putVegetables(vegetables) { this.sandwich.addIngredient(vegetables); }
  get result() { return this.sandwich; }
}

class SandwichMaker {
  constructor() { this.builder = new SandwichBuilder(); }
  createCheeseSteakSandwich() {
    this.builder.reset();
    this.builder.putMeat('ribeye steak');
    this.builder.putCheese('american cheese');
    this.builder.putVegetables(['peppers', 'onions']);
    return this.builder.result;
  }
}

const sandwichMaker = new SandwichMaker();
const sandwich = sandwichMaker.createCheeseSteakSandwich();
console.log(`Your sandwich: ${sandwich}`); // Your sandwich: ribeye steak, american cheese, peppers, onions

Prototype Pattern creates new objects by cloning an existing prototype. The example clones a carPrototype using Object.create , then modifies properties of the clones.

const carPrototype = { wheels: 4, color: 'red', start() { console.log('Starting the car...'); }, stop() { console.log('Stopping the car...'); } };
const car1 = Object.create(carPrototype);
car1.wheels = 6;
car1.color = 'red';
car1.start(); // Starting the car...
car1.stop();  // Stopping the car...
const car2 = Object.create(carPrototype);
car2.color = 'blue';
car2.start(); // Starting the car...
car2.stop();  // Stopping the car...

Structural patterns demonstrated include Adapter, Decorator, Proxy, Facade, Bridge, Composite, and Flyweight. Each pattern is described and accompanied by JavaScript code.

Adapter Pattern allows incompatible interfaces to work together.

class Target { request() { console.log('Target: 请求已被调用'); } }
class Adaptee { specificRequest() { console.log('Adaptee 方法已被访问'); } }
class Adapter extends Target { constructor(adaptee) { super(); this.adaptee = adaptee; }
  request() { this.adaptee.specificRequest(); } }
const client = new Adapter(new Adaptee());
client.request(); // Adaptee 方法已被访问

Decorator Pattern adds responsibilities to objects dynamically.

class Component { operation() { console.log('Component:基础操作'); } }
class ConcreteComponent extends Component { operation() { console.log('ConcreteComponent:具体操作'); } }
class Decorator extends Component { constructor(component) { super(); this.component = component; }
  operation() { this.component.operation(); } }
class ConcreteDecoratorA extends Decorator { operation() { super.operation(); console.log('ConcreteDecoratorA:添加操作'); } }
class ConcreteDecoratorB extends Decorator { operation() { super.operation(); console.log('ConcreteDecoratorB:添加操作'); } }
const component = new ConcreteComponent();
const decoratorA = new ConcreteDecoratorA(component);
const decoratorB = new ConcreteDecoratorB(decoratorA);
decoratorB.operation();

Proxy Pattern provides a placeholder for another object to control access.

class Subject { request() { console.log('Subject:处理请求'); } }
class RealSubject extends Subject { request() { console.log('RealSubject:处理请求'); } }
class Proxy extends Subject { constructor(realSubject) { super(); this.realSubject = realSubject; }
  request() { if (this.checkAccess()) { this.realSubject.request(); this.logAccess(); } }
  checkAccess() { console.log('Proxy:检查访问权限'); return true; }
  logAccess() { console.log('Proxy:记录访问日志'); } }
const realSubject = new RealSubject();
const proxy = new Proxy(realSubject);
proxy.request();

Facade Pattern offers a simple interface to a complex subsystem.

class Subsystem1 { operation1() { console.log('Subsystem1:执行操作1'); } }
class Subsystem2 { operation2() { console.log('Subsystem2:执行操作2'); } }
class Facade { constructor() { this.subsystem1 = new Subsystem1(); this.subsystem2 = new Subsystem2(); }
  operation() { this.subsystem1.operation1(); this.subsystem2.operation2(); } }
const facade = new Facade();
facade.operation(); // Subsystem1:执行操作1, Subsystem2:执行操作2

Bridge Pattern decouples an abstraction from its implementation.

class Implementor { operationImpl() { console.log('Implementor:执行操作'); } }
class Abstraction { constructor(implementor) { this.implementor = implementor; }
  operation() { this.implementor.operationImpl(); } }
class RefinedAbstraction extends Abstraction { otherOperation() { console.log('RefinedAbstraction:其他操作'); } }
const implementor = new Implementor();
const abstraction = new Abstraction(implementor);
abstraction.operation(); // Implementor:执行操作
const refined = new RefinedAbstraction(implementor);
refined.operation(); // Implementor:执行操作
refined.otherOperation(); // RefinedAbstraction:其他操作

Composite Pattern composes objects into tree structures to represent part‑whole hierarchies.

class Component { constructor(name) { this.name = name; }
  operation() { console.log(`Component ${this.name}:执行操作`); }
  add() { console.log('Component:不支持的操作'); }
  remove() { console.log('Component:不支持的操作'); }
  getChild() { console.log('Component:不支持的操作'); } }
class Leaf extends Component {}
class Composite extends Component { constructor(name) { super(name); this.children = []; }
  add(component) { this.children.push(component); }
  remove(component) { const index = this.children.indexOf(component); if (index >= 0) this.children.splice(index, 1); }
  getChild(index) { return this.children[index]; } }
const root = new Composite('根');
const branch1 = new Composite('树枝1');
const branch2 = new Composite('树枝2');
const leaf1 = new Leaf('叶子1');
const leaf2 = new Leaf('叶子2');
const leaf3 = new Leaf('叶子3');
root.add(branch1); root.add(branch2);
branch1.add(leaf1); branch1.add(leaf2);
branch2.add(leaf3);
root.operation(); // Component 根:执行操作
branch1.operation(); // Component 树枝1:执行操作
branch1.remove(leaf2);
branch2.operation(); // Component 树枝2:执行操作
root.getChild(0).operation(); // Component 树枝1:执行操作

Flyweight Pattern shares objects to minimize memory usage.

class FlyweightFactory { constructor() { this.flyweights = {}; }
  getFlyweight(key) { if (!this.flyweights[key]) { this.flyweights[key] = new ConcreteFlyweight(key); } return this.flyweights[key]; } }
class ConcreteFlyweight { constructor(key) { this.key = key; }
  operation() { console.log(`ConcreteFlyweight ${this.key}: 执行操作`); } }
const factory = new FlyweightFactory();
const flyweight1 = factory.getFlyweight('key');
const flyweight2 = factory.getFlyweight('key');
flyweight1.operation(); // ConcreteFlyweight key: 执行操作
flyweight2.operation(); // ConcreteFlyweight key: 执行操作
console.log(flyweight1 === flyweight2); // true

Behavioral patterns covered include Strategy, Template Method, Observer, Iterator, Chain of Responsibility, Command, Memento, State, Visitor, Mediator, and Interpreter.

Strategy Pattern defines a family of algorithms and makes them interchangeable.

class Strategy { constructor(name) { this.name = name; } execute() {} }
class StrategyA extends Strategy { execute() { console.log('Executing strategy A'); } }
class StrategyB extends Strategy { execute() { console.log('Executing strategy B'); } }
class Context { constructor(strategy) { this.strategy = strategy; }
  executeStrategy() { this.strategy.execute(); } }
let context = new Context(new StrategyA('A'));
context.executeStrategy(); // Executing strategy A
context.strategy = new StrategyB('B');
context.executeStrategy(); // Executing strategy B

Template Method Pattern defines the skeleton of an algorithm in a method, deferring some steps to subclasses.

class Game { setup() {} play() {} finish() {} start() { this.setup(); this.play(); this.finish(); } }
class Chess extends Game { setup() { console.log('Setting up chess game'); }
  play() { console.log('Playing chess'); }
  finish() { console.log('Finishing chess game'); } }
class TicTacToe extends Game { setup() { console.log('Setting up TicTacToe game'); }
  play() { console.log('Playing TicTacToe'); }
  finish() { console.log('Finishing TicTacToe game'); } }
let game = new Chess();
game.start();
game = new TicTacToe();
game.start();

Observer Pattern defines a one‑to‑many dependency so that when one object changes state, all its dependents are notified.

class Subject { constructor() { this.observers = []; }
  attach(observer) { this.observers.push(observer); }
  detach(observer) { const index = this.observers.indexOf(observer); if (index > -1) this.observers.splice(index, 1); }
  notify() { for (const observer of this.observers) { observer.update(this); } } }
class Observer { update(subject) {} }
class ConcreteSubject extends Subject { constructor(state) { super(); this.state = state; }
  set_state(state) { this.state = state; this.notify(); }
  get_state() { return this.state; } }
class ConcreteObserver extends Observer { update(subject) { console.log(`Got updated value: ${subject.get_state()}`); } }
let subject = new ConcreteSubject('initial state');
let observer = new ConcreteObserver();
subject.attach(observer);
subject.set_state('new state');

Iterator Pattern provides a way to access elements of an aggregate object sequentially without exposing its underlying representation.

class Iterator { constructor(items) { this.items = items; this.cursor = 0; }
  has_next() { return this.cursor < this.items.length; }
  next() { return this.items[this.cursor++]; } }
class Collection { constructor() { this.items = []; }
  add_item(item) { this.items.push(item); }
  iterator() { return new Iterator(this.items); } }
const collection = new Collection();
collection.add_item('item 1');
collection.add_item('item 2');
collection.add_item('item 3');
const iterator = collection.iterator();
while (iterator.has_next()) { console.log(iterator.next()); }

Chain of Responsibility Pattern passes a request along a chain of handlers until one handles it.

class Handler { constructor() { this.nextHandler = null; }
  setNextHandler(handler) { this.nextHandler = handler; }
  handleRequest(request) { if (this.nextHandler !== null) return this.nextHandler.handleRequest(request); return null; } }
class ConcreteHandlerA extends Handler { handleRequest(request) { if (request === 'A') return `Handle Request ${request}`; return super.handleRequest(request); } }
class ConcreteHandlerB extends Handler { handleRequest(request) { if (request === 'B') return `Handle Request ${request}`; return super.handleRequest(request); } }
class ConcreteHandlerC extends Handler { handleRequest(request) { if (request === 'C') return `Handle Request ${request}`; return super.handleRequest(request); } }
const handlerA = new ConcreteHandlerA();
const handlerB = new ConcreteHandlerB();
const handlerC = new ConcreteHandlerC();
handlerA.setNextHandler(handlerB);
handlerB.setNextHandler(handlerC);
console.log(handlerA.handleRequest('A')); // Handle Request A
console.log(handlerA.handleRequest('B')); // Handle Request B
console.log(handlerA.handleRequest('C')); // Handle Request C
console.log(handlerA.handleRequest('D')); // null

Command Pattern encapsulates a request as an object, thereby allowing for parameterization and queuing of requests.

class Command { constructor(receiver) { this.receiver = receiver; }
  execute() { throw new Error('You have to implement the method execute!'); } }
class ConcreteCommandA extends Command { execute() { this.receiver.actionA(); } }
class ConcreteCommandB extends Command { execute() { this.receiver.actionB(); } }
class Receiver { actionA() { console.log('Receiver Action A.'); } actionB() { console.log('Receiver Action B.'); } }
class Invoker { constructor() { this.commands = new Map(); }
  setCommand(key, command) { this.commands.set(key, command); }
  executeCommand(key) { const command = this.commands.get(key); if (!command) { console.log(`Command ${key} is not found.`); return; } command.execute(); } }
const receiver = new Receiver();
const invoker = new Invoker();
invoker.setCommand('A', new ConcreteCommandA(receiver));
invoker.setCommand('B', new ConcreteCommandB(receiver));
invoker.executeCommand('A'); // Receiver Action A.
invoker.executeCommand('B'); // Receiver Action B.

Memento Pattern captures and restores an object's internal state without violating encapsulation.

class Memento { constructor(state) { this.state = state; }
  getState() { return this.state; } }
class Originator { constructor(state) { this.state = state; }
  setState(state) { this.state = state; }
  createMemento() { return new Memento(this.state); }
  restoreMemento(memento) { this.state = memento.getState(); }
  getState() { return this.state; } }
class Caretaker { constructor() { this.mementos = []; }
  addMemento(memento) { this.mementos.push(memento); }
  getMemento(index) { return this.mementos[index]; } }
const originator = new Originator('State A');
const caretaker = new Caretaker();
caretaker.addMemento(originator.createMemento());
originator.setState('State B');
console.log(`Current State: ${originator.getState()}`); // State B
originator.restoreMemento(caretaker.getMemento(0));
console.log(`Current State: ${originator.getState()}`); // State A

State Pattern allows an object to alter its behavior when its internal state changes.

class Context { constructor() { this.state = new ConcreteStateA(this); }
  setState(state) { this.state = state; }
  request() { this.state.handle(); } }
class State { constructor(context) { this.context = context; }
  handle() { throw new Error('You have to implement the method handle!'); } }
class ConcreteStateA extends State { handle() { console.log('Handle State A'); this.context.setState(new ConcreteStateB(this.context)); } }
class ConcreteStateB extends State { handle() { console.log('Handle State B'); this.context.setState(new ConcreteStateA(this.context)); } }
const context = new Context();
context.request(); // Handle State A
context.request(); // Handle State B
context.request(); // Handle State A

Visitor Pattern lets you define new operations on elements without changing their classes.

class Element { accept(visitor) { throw new Error('You have to implement the method accept!'); } }
class ConcreteElementA extends Element { accept(visitor) { visitor.visitConcreteElementA(this); }
  operationA() { console.log('Operation A of Concrete Element A.'); } }
class ConcreteElementB extends Element { accept(visitor) { visitor.visitConcreteElementB(this); }
  operationB() { console.log('Operation B of Concrete Element B.'); } }
class Visitor { visitConcreteElementA(element) { console.log(`Visit Concrete Element A with ${element.operationA()}`); }
  visitConcreteElementB(element) { console.log(`Visit Concrete Element B with ${element.operationB()}`); } }
const elementA = new ConcreteElementA();
const elementB = new ConcreteElementB();
const visitor = new Visitor();
elementA.accept(visitor);
elementB.accept(visitor);

Mediator Pattern reduces direct dependencies between components by introducing a mediator.

class Mediator { constructor() { this.components = new Set(); }
  register(component) { component.mediator = this; this.components.add(component); }
  notify(sender, event) { this.components.forEach(component => { if (component !== sender) component.receive(sender, event); }); } }
class Component { constructor(name) { this.name = name; this.mediator = null; }
  send(event) { console.log(`Send event ${event} from ${this.name}`); this.mediator.notify(this, event); }
  receive(sender, event) { console.log(`Receive event ${event} from ${sender.name} by ${this.name}`); } }
const mediator = new Mediator();
const componentA = new Component('Component A');
const componentB = new Component('Component B');
const componentC = new Component('Component C');
mediator.register(componentA);
mediator.register(componentB);
mediator.register(componentC);
componentA.send('Hello');

Interpreter Pattern defines a representation for a grammar and an interpreter that uses the representation to interpret sentences in the language.

class Context { constructor(input) { this.input = input; this.output = 0; } }
class Expression { interpreter(context) { throw new Error('You have to implement the method interpreter!'); } }
class ThousandExpression extends Expression { interpreter(context) { if (context.input.startsWith('M')) { context.output += 1000; context.input = context.input.slice(1); } } }
class HundredExpression extends Expression { interpreter(context) { const str = context.input; if (str.startsWith('C')) { context.output += 100; context.input = str.slice(1); }
  else if (str.startsWith('CD')) { context.output += 400; context.input = str.slice(2); }
  else if (str.startsWith('CM')) { context.output += 900; context.input = str.slice(2); } } }
class TenExpression extends Expression { interpreter(context) { const str = context.input; if (str.startsWith('X')) { context.output += 10; context.input = str.slice(1); }
  else if (str.startsWith('XL')) { context.output += 40; context.input = str.slice(2); }
  else if (str.startsWith('XC')) { context.output += 90; context.input = str.slice(2); } } }
class OneExpression extends Expression { interpreter(context) { const str = context.input; if (str.startsWith('I')) { context.output += 1; context.input = str.slice(1); }
  else if (str.startsWith('IV')) { context.output += 4; context.input = str.slice(2); }
  else if (str.startsWith('V')) { context.output += 5; context.input = str.slice(1); }
  else if (str.startsWith('IX')) { context.output += 9; context.input = str.slice(2); } } }
class Interpreter {
  static parse(roman) {
    const context = new Context(roman);
    const tree = [new ThousandExpression(), new HundredExpression(), new TenExpression(), new OneExpression()];
    tree.forEach(expression => expression.interpreter(context));
    return context.output;
  }
}
console.log(Interpreter.parse('CDXLVIII')); // 448

The article concludes by encouraging readers to apply the learned patterns to transform vague code into well‑structured, understandable implementations.

design patternsJavaScriptsingletonadapterdecoratorPrototypeFactory MethodBuilder
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.