GoF Design Patterns: Core Concepts for Exams (Singleton, Factory Method, Builder, Prototype, Abstract Factory)

Design PatternsGoFSingletonFactory MethodBuilderPrototypeAbstract FactoryObject-OrientedOCPDevelopment
Read in about 9 min read
Published: 2025-07-02
Last modified: 2025-07-03
View count: 40

Summary

Learn the core concepts of the Gang of Four (GoF) design patterns, focusing on creational patterns like Singleton, Factory Method, Builder, Prototype, and Abstract Factory with code examples. Understand how to write scalable and flexible code.

🎯 What are GoF Design Patterns?

GoF stands for 'Gang of Four,' referring to the four authors of the book on design patterns that provides solutions to common problems in object-oriented programming. The patterns they compiled are a great help in increasing code reusability, maintainability, and scalability. ✨

Design patterns are broadly divided into three categories: Creational, Structural, and Behavioral. In this article, we will focus on the Creational Patterns, which deal with object creation. 🏗️

1. 👑 Singleton Pattern

The Singleton pattern, as the name suggests, is a design pattern that enforces the existence of only one object per class. The key is that the instance is unique, as implied by the word single. 👑

🏗️ Basic Structure

In class-based languages, a singleton is typically implemented as follows:

javascript
class Singleton {
  constructor() {
    // Check if an instance already exists.
    if (Singleton.instance) {
      return Singleton.instance;
    }

    this.message = "I am a class-based unique instance";
    // If not, store the current instance.
    Singleton.instance = this;
  }
}

// Usage Example
const a = new Singleton();
const b = new Singleton();

console.log(a === b); // true ✅
console.log(a.message); // "I am a class-based unique instance"

Although new Singleton() was called twice, the variables a and b refer to the exact same instance. 🎯

🤔 When is it needed?

It is useful when there is a resource that needs to be shared globally across the application. A typical example is a Logger. 📝

If there are multiple Logger instances, logs might be recorded in a distributed manner. ⚠️

javascript
class Logger {
  constructor(name) {
    this.name = name;
    this.logs = [];
  }

  log(message) {
    this.logs.push(`[${this.name}] ${message}`);
  }

  printLogs() {
    console.log(this.logs.join("\n"));
  }
}

// Case where two instances are created 😵
const loggerA = new Logger("A");
const loggerB = new Logger("B");

loggerA.log("Server started");
loggerB.log("DB connected");
loggerA.log("User logged in");

// Logs are stored separately in each instance. 😞
loggerA.printLogs();
// [A] Server started
// [A] User logged in

loggerB.printLogs();
// [B] DB connected

By applying the Singleton pattern to the Logger, even if instances are created in multiple places, only one instance is actually used, allowing for centralized log management. ✅

javascript
class Logger {
  constructor() {
    if (Logger.instance) {
      return Logger.instance;
    }
    this.logs = [];
    Logger.instance = this;
  }

  log(message) {
    this.logs.push(message);
  }

  printLogs() {
    console.log(this.logs.join("\n"));
  }
}

// It's okay to call Logger from multiple places. 😊
const logger1 = new Logger();
const logger2 = new Logger();

logger1.log("Server started");
logger2.log("DB connected");
logger1.log("User logged in");

// All logs are recorded in a single instance. 🎉
logger1.printLogs();
/*
Server started
DB connected
User logged in
*/

console.log(logger1 === logger2); // true ✅

💡 Singleton Pattern Keywords

  • Enforces that a class has only one instance. 👑

2. 🏭 Factory Method Pattern

The Factory Method pattern is a pattern where the interface for creating objects is defined in a superclass, but the decision of which class's instance to create is left to the subclasses. ⚙️

This pattern involves two main structures: Creator and Product.

  • Product: Defines the common interface for the objects to be created. 📋
  • Creator: Defines the factoryMethod() that creates a Product. The actual creation logic is handled by the subclass Creator. 🏗️

🏗️ Basic Structure

Let's take a payment system as an example. There are various payment methods (credit card, PayPal, etc.), but the overall flow of 'payment processing' is the same. 💳

javascript
// Product Interface (Payment Gateway)
class PaymentGateway {
  pay(amount) {
    throw new Error("pay() must be implemented.");
  }
}

// Concrete Product 1 (Card Payment)
class CardGateway extends PaymentGateway {
  pay(amount) {
    console.log(`💳 Paid ${amount} with credit card`);
  }
}

// Concrete Product 2 (PayPal Payment)
class PaypalGateway extends PaymentGateway {
  pay(amount) {
    console.log(`🅿️ Paid ${amount} with PayPal`);
  }
}

// Creator (Parent Payment Processor)
class PaymentProcessor {
  // This method does not depend on the specific payment method. 🎯
  process(amount) {
    const gateway = this.createGateway(); // Call Factory Method
    gateway.pay(amount);
  }

  // Factory Method (Subclasses implement the specifics)
  createGateway() {
    throw new Error("createGateway() must be overridden.");
  }
}

// Concrete Creator 1 (Card Payment Processor)
class CardPaymentProcessor extends PaymentProcessor {
  createGateway() {
    return new CardGateway();
  }
}

// Concrete Creator 2 (PayPal Payment Processor)
class PaypalPaymentProcessor extends PaymentProcessor {
  createGateway() {
    return new PaypalGateway();
  }
}

// Usage Example ✨
const cardProcessor = new CardPaymentProcessor();
cardProcessor.process(10000); // 💳 Paid 10000 with credit card

const paypalProcessor = new PaypalPaymentProcessor();
paypalProcessor.process(20000); // 🅿️ Paid 20000 with PayPal

🚀 Scalability

The biggest advantage of the Factory Method is scalability. If a new payment method, 'KakaoPay,' needs to be added, there is no need to modify the existing code at all. 🎉

javascript
// New Concrete Product
class KakaoPayGateway extends PaymentGateway {
  pay(amount) {
    console.log(`💛 Paid ${amount} with KakaoPay`);
  }
}

// New Concrete Creator
class KakaoPayPaymentProcessor extends PaymentProcessor {
  createGateway() {
    return new KakaoPayGateway();
  }
}

// New functionality can be used immediately 🚀
const kakaoProcessor = new KakaoPayPaymentProcessor();
kakaoProcessor.process(15000); // 💛 Paid 15000 with KakaoPay

What if we handled this with a simple if/switch statement instead of the Factory Method? 🤔

javascript
function createGateway(type) {
  if (type === "card") return new CardGateway();
  if (type === "paypal") return new PaypalGateway();
  // When adding 'KakaoPay', modifying the code below is unavoidable. 😰
  if (type === "kakaopay") return new KakaoPayGateway(); // ❌ OCP violation
}

✅ Core Concept: OCP (Open-Closed Principle)

The Factory Method demonstrates one of the five principles of object-oriented design (SOLID), the OCP. 📐

  • Open for extension: New payment methods (KakaoPay) can be easily added. ➕
  • Closed for modification: There's no need to modify existing PaymentProcessor or other payment-related classes to add new functionality. 🔒

This way, using the Factory Method pattern allows you to create systems that are flexible to change and have good scalability. 💪

💡 Factory Method Pattern Keywords

  • Define an interface for creating objects in a superclass 🏗️
  • Create instances in subclasses ⚙️
  • Separate the interface from the classes that actually create objects 🔀

3. 🔨 Builder Pattern

The Builder pattern is a design pattern that separates the construction of complex objects step by step, allowing the same construction process to create different representation results. It's particularly useful when constructor parameters are numerous or when the object creation process is complex with multiple steps. 🏗️

😵 Have you ever seen code like this?

When there are too many parameters in a constructor when creating an object, it's sometimes called the 'Telescoping Constructor Pattern'.

javascript
// 💩 Constructor with too many parameters
class HttpRequest {
  constructor(method, url, headers, body, timeout, retries, cache, useHttps) {
    this.method = method;
    this.url = url;
    this.headers = headers || {};
    this.body = body || null;
    this.timeout = timeout || 10000;
    this.retries = retries || 3;
    this.cache = cache || false;
    this.useHttps = useHttps || true;
  }
}

// Very inconvenient to use. 😫
const req = new HttpRequest(
  "GET",
  "https://api.example.com/data",
  { "Content-Type": "application/json" },
  null,
  15000,
  5,
  true,
  true
);

The problems with the above code are: ⚠️

  • Poor readability: It's hard to understand what each parameter does. 😵
  • Error-prone: It's easy to input parameters in the wrong order. 🔀
  • Lack of flexibility: Even if you want to set only some parameters, you have to keep passing null or undefined. 😓

✨ Improving with Builder Pattern

The Builder pattern can solve this problem elegantly.

javascript
// Product (Result)
class HttpRequest {
  constructor(builder) {
    this.method = builder.method;
    this.url = builder.url;
    this.headers = builder.headers;
    this.body = builder.body;
    this.timeout = builder.timeout;
    this.retries = builder.retries;
    this.cache = builder.cache;
    this.useHttps = builder.useHttps;
  }

  send() {
    console.log(`[${this.method}] ${this.url} request sent 🚀`);
    // ... actual request logic ...
  }
}

// Builder
class HttpRequestBuilder {
  constructor() {
    // Set default values ⚙️
    this.method = "GET";
    this.headers = {};
    this.timeout = 10000;
    this.retries = 3;
    this.cache = false;
    this.useHttps = true;
  }

  // Each setting method returns the builder instance itself (this). (Method chaining) 🔗
  setMethod(method) {
    this.method = method;
    return this; // Return self to enable method chaining
  }

  setUrl(url) {
    this.url = url;
    return this;
  }

  setHeaders(headers) {
    this.headers = headers;
    return this;
  }

  setBody(body) {
    this.body = body;
    return this;
  }

  setTimeout(timeout) {
    this.timeout = timeout;
    return this;
  }

  // ... other setting methods ...

  // Finally, the build() method creates the HttpRequest object. 🏗️
  build() {
    if (!this.url) {
      throw new Error("URL is required. ⚠️");
    }
    return new HttpRequest(this);
  }
}

// ✨ Usage is much clearer and more flexible.
const request = new HttpRequestBuilder()
  .setMethod("POST")
  .setUrl("https://api.example.com/users")
  .setHeaders({ "Content-Type": "application/json" })
  .setBody({ name: "John Doe" })
  .setTimeout(5000)
  .build();

request.send(); // [POST] https://api.example.com/users request sent 🚀

Through the above example, we learned about the Builder pattern that separates the method of creating objects (HttpRequestBuilder class) from the method of implementing objects (HttpRequest class) when creating complex objects (many properties, nested structures...). 🎯

💡 Builder Pattern Keywords

  • When creating complex objects 🏗️

  • Method of creating objects (process) ⚙️

  • Method of implementing (representing) objects 📋

  • Separate the two methods 🔀

4. 🧬 Prototype Pattern

The Prototype pattern is a design pattern that creates new objects by cloning existing objects. Instead of using the new keyword to create objects from classes, it copies a prototype object and modifies the necessary parts before use. It's particularly useful when object creation is expensive or when the process of creating objects from classes is complex. 💡

JavaScript is a prototype-based language, so this pattern can be utilized very naturally. 🚀

🏗️ Basic Structure

Let's look at an example of creating game characters. All characters share basic stats (health, mana), but each can have different names and jobs. ⚔️

javascript
// Prototype (Original object)
const characterPrototype = {
  hp: 100,
  mp: 50,
  attack(target) {
    console.log(`${this.name} attacks ${target}! ⚔️`);
  },
  // Method to clone the object
  clone() {
    // Use Object.create() to create a new object based on the prototype. 🧬
    return Object.create(this);
  },
};

// Create new objects by cloning the prototype ✨
const warrior = characterPrototype.clone();
warrior.name = "Strong Warrior";
warrior.job = "Warrior";
warrior.hp = 150; // Change from default value

const mage = characterPrototype.clone();
mage.name = "Wise Mage";
mage.job = "Mage";
mage.mp = 120; // Change from default value

warrior.attack("Orc"); // Strong Warrior attacks Orc! ⚔️
mage.attack("Goblin"); // Wise Mage attacks Goblin! ⚔️

console.log(warrior.hp); // 150 💪
console.log(mage.hp); // 100 (uses the original prototype value) ✅

Object.create() creates a new object using the specified prototype object. Objects created this way inherit the properties and methods of the original while being able to have their own unique state. 🎯

🤔 When is it needed?

  • When object creation is expensive: If you need to query heavy data from a database to create objects, you can gain performance benefits by cloning a once-created object instead of creating it every time. 💾
  • When you need to create various types of objects: Without creating many subclasses like the Factory Method pattern, you can easily create various variant objects with just a few prototype objects. 🎨

In JavaScript, the Prototype pattern can be implemented simply through Object.create() or spread syntax ({...original, ...overrides}). ⚡

💡 Prototype Pattern Keywords

  • Clone original objects to create new objects. 🧬

  • Can create objects without the new keyword. ✨

  • Effective when object creation is expensive. 💰

5. 🏪 Abstract Factory Pattern

The Abstract Factory pattern provides an interface for creating sets of related or dependent objects without specifying their concrete classes. While the Factory Method pattern focuses on creating a single object, Abstract Factory is used to create 'families of products'. 👨‍👩‍👧‍👦

The most common example is managing UI component themes. According to light mode and dark mode, multiple UI elements like buttons, checkboxes, windows, etc., need to have consistent styles. 🎨

🏗️ Basic Structure

javascript
// Abstract Product A: Button
class Button {
  paint() {
    throw new Error("paint() must be implemented.");
  }
}

// Concrete Product A1: LightButton
class LightButton extends Button {
  paint() {
    console.log("🎨 Drawing light theme button. ☀️");
  }
}

// Concrete Product A2: DarkButton
class DarkButton extends Button {
  paint() {
    console.log("🎨 Drawing dark theme button. 🌙");
  }
}

// Abstract Product B: Checkbox
class Checkbox {
  paint() {
    throw new Error("paint() must be implemented.");
  }
}

// Concrete Product B1: LightCheckbox
class LightCheckbox extends Checkbox {
  paint() {
    console.log("✅ Drawing light theme checkbox. ☀️");
  }
}

// Concrete Product B2: DarkCheckbox
class DarkCheckbox extends Checkbox {
  paint() {
    console.log("✅ Drawing dark theme checkbox. 🌙");
  }
}

// Abstract Factory
class GUIFactory {
  createButton() {
    throw new Error("createButton() must be implemented.");
  }
  createCheckbox() {
    throw new Error("createCheckbox() must be implemented.");
  }
}

// Concrete Factory 1: LightThemeFactory
class LightThemeFactory extends GUIFactory {
  createButton() {
    return new LightButton();
  }
  createCheckbox() {
    return new LightCheckbox();
  }
}

// Concrete Factory 2: DarkThemeFactory
class DarkThemeFactory extends GUIFactory {
  createButton() {
    return new DarkButton();
  }
  createCheckbox() {
    return new DarkCheckbox();
  }
}

// Client Code 🎯
function renderUI(factory) {
  const button = factory.createButton();
  const checkbox = factory.createCheckbox();
  button.paint();
  checkbox.paint();
}

// Render UI with light theme ☀️
console.log("--- Light Mode UI ---");
const lightFactory = new LightThemeFactory();
renderUI(lightFactory);
// 🎨 Drawing light theme button. ☀️
// ✅ Drawing light theme checkbox. ☀️

// Render UI with dark theme 🌙
console.log("\n--- Dark Mode UI ---");
const darkFactory = new DarkThemeFactory();
renderUI(darkFactory);
// 🎨 Drawing dark theme button. 🌙
// ✅ Drawing dark theme checkbox. 🌙

🆚 Factory Method vs. Abstract Factory

  • Factory Method: One Creator class changes the way one product is created through inheritance. 🏭
  • Abstract Factory: Provides an interface for creating multiple product families. Clients can change the entire product family at once just by replacing the factory, without needing to know specific product classes. 🏪

💡 Abstract Factory Pattern Keywords

  • Creates 'families' of related objects. 👨‍👩‍👧‍👦
  • Client code does not depend on specific classes. 🔒
  • Can easily replace entire product families. 🔄
  • Provides API to users, and specific implementation is done in Concrete Product classes. 🎯

🤔 Meaning of 'API Provision' and 'Specific Implementation'

Here, "providing API to users" means the 'users' are not actual people, but client code that uses the factory.

  • API Provision (Abstract Classes): Abstract classes like GUIFactory, Button, Checkbox serve as a kind of 'contract' or 'manual'. 📋 Client code (the renderUI function) only looks at this 'manual' and calls methods like createButton() or paint(). The client doesn't need to know the actual implementation of LightButton or DarkButton at all. 🔒
  • Specific Implementation (Concrete Classes): Concrete classes like LightThemeFactory or DarkButton are 'implementers' who actually fulfill the contents of the 'contract'. 👷‍♂️ When a createButton() request comes, they create actual light/dark theme button objects, and when a paint() request comes, they perform actual theme-specific styling logic.

In conclusion, client code doesn't need to directly write specific class names (LightButton) in the code and only depends on the abstract 'manual (API)'. Thanks to this, a flexible structure is created where just by replacing the factory, the entire product family (theme) can be changed at once. 🔄✨