GoF Design Patterns: Structural Patterns (2) - Key Concepts for Exams (Composite, Bridge, Flyweight)

Design PatternsGoFCompositeBridgeFlyweightStructural PatternsDevelopment
Read in about 8 min read
Published: 2025-07-04
Last modified: 2025-07-04
View count: 19

Summary

Explore the Gang of Four (GoF) structural design patterns—Composite, Bridge, and Flyweight—that compose classes and objects into larger structures, with JavaScript code examples.

🧩 5. Composite Pattern

The Composite pattern is a design pattern that composes objects into tree structures to represent part-whole hierarchies. This pattern allows clients to treat individual objects (Leaves) and compositions of objects (Composites) uniformly. In other words, you can control a single object or a collection of objects through the same interface.

A classic example is a computer's file system. A folder (Composite) can contain other folders or files (Leaves). The user can perform the same operations, like 'rename' or 'delete', on both folders and files without distinction.

🏗️ Basic Structure

  • Component: A common interface that both Leaf and Composite must implement. It defines consistent operations for all objects in the tree structure.
  • Leaf: An individual object at the end of a tree. It has no children. (e.g., a file)
  • Composite: A complex object that can have children (Leaves or other Composites). It implements methods to manage its children (add, remove, etc.) and recursively passes calls from the Component interface to its children. (e.g., a folder)

💻 Example: Representing a File System

Let's implement a simple file system composed of files and folders using the Composite pattern.

javascript
// Component: Common interface
class FileSystemComponent {
  constructor(name) {
    this.name = name;
  }

  // Common operation for all components
  display(indent = "") {
    throw new Error("The display() method must be implemented.");
  }

  // Methods meaningful only for Composite objects
  add(component) {
    throw new Error("Cannot add children to this object.");
  }

  remove(component) {
    throw new Error("Cannot remove children from this object.");
  }
}

// Leaf: Individual file
class File extends FileSystemComponent {
  constructor(name, size) {
    super(name);
    this.size = size;
  }

  display(indent = "") {
    console.log(`${indent}- ${this.name} (${this.size}KB)`);
  }
}

// Composite: Folder
class Folder extends FileSystemComponent {
  constructor(name) {
    super(name);
    this.children = [];
  }

  add(component) {
    this.children.push(component);
  }

  remove(component) {
    const index = this.children.indexOf(component);
    if (index > -1) {
      this.children.splice(index, 1);
    }
  }

  display(indent = "") {
    console.log(`${indent}+ ${this.name}`);
    // Recursively call display on children
    this.children.forEach((child) => {
      child.display(indent + "  ");
    });
  }
}

// Client Code
const root = new Folder("root");
const documents = new Folder("Documents");
const music = new Folder("Music");

const file1 = new File("Resume.docx", 120);
const file2 = new File("Project_Plan.pdf", 340);
const song1 = new File("IU - Through the Night.mp3", 4500);

documents.add(file1);
documents.add(file2);
music.add(song1);

root.add(documents);
root.add(music);
root.add(new File("important_file.txt", 50));

// Print the entire file system structure
root.display();

Output:

text
+ root
  + Documents
    - Resume.docx (120KB)
    - Project_Plan.pdf (340KB)
  + Music
    - IU - Through the Night.mp3 (4500KB)
  - important_file.txt (50KB)

The client only called display() on the root folder object, yet the entire tree structure was printed recursively. The client doesn't need to worry about the internal differences between File and Folder; it can interact with them uniformly through the single FileSystemComponent interface.

Composite Pattern Keywords 🔑

  • 🌳 Represents part-whole hierarchies using a tree structure.
  • 🎭 Treats single objects (Leaves) and composite objects (Composites) uniformly.
  • 🔄 Simplifies client code through a recursive structure.
  • 🧩 Very useful for handling hierarchical structures like UI toolkits and file systems.

🌉 6. Bridge Pattern

The Bridge pattern is a design pattern that decouples an 'abstraction' from its 'implementation' into two independent class hierarchies, allowing them to be extended independently without affecting each other. As its name suggests, it acts as a 'bridge' connecting the two domains.

Using inheritance can lead to a strong coupling between functionality and implementation. When you need to add a new feature or support a new implementation method, the number of classes can grow exponentially. The Bridge pattern solves this problem by using Composition instead of inheritance.

🏗️ Basic Structure

  • Abstraction: Defines the high-level interface for clients. It holds a reference to an Implementor.
  • RefinedAbstraction: Extends the Abstraction to provide more specific functionality.
  • Implementor: Defines the low-level interface for the actual implementation. The Abstraction depends only on this interface.
  • ConcreteImplementor: The class that actually implements the Implementor interface.

💻 Example: UI Elements with Various Themes

Imagine a situation where different types of UI elements (e.g., buttons, toggle switches) need to support various themes (e.g., light mode, dark mode).

First, let's create the Theme interface and concrete theme classes that will implement it.

javascript
// Implementor: Theme interface
class Theme {
  getColor() {
    throw new Error("The getColor() method must be implemented.");
  }
}

// ConcreteImplementor A: Light Theme
class LightTheme extends Theme {
  getColor() {
    return "White";
  }
}

// ConcreteImplementor B: Dark Theme
class DarkTheme extends Theme {
  getColor() {
    return "Black";
  }
}

Now, let's create an abstract UIComponent class to represent UI elements. This class will contain a Theme object (Composition).

javascript
// Abstraction: Abstract UI element class
class UIComponent {
  constructor(theme) {
    this.theme = theme;
  }

  render() {
    throw new Error("The render() method must be implemented.");
  }
}

Let's create concrete UI elements that inherit from UIComponent.

javascript
// RefinedAbstraction A: Button
class Button extends UIComponent {
  render() {
    console.log(`Rendering a button. (Background: ${this.theme.getColor()})`);
  }
}

// RefinedAbstraction B: Toggle Switch
class Toggle extends UIComponent {
  render() {
    console.log(
      `Rendering a toggle switch. (Background: ${this.theme.getColor()})`
    );
  }
}

Now, the client code can combine them for use.

javascript
// Client Code
const lightTheme = new LightTheme();
const darkTheme = new DarkTheme();

// UI elements using the light theme
const lightButton = new Button(lightTheme);
const lightToggle = new Toggle(lightTheme);

lightButton.render(); // Output: Rendering a button. (Background: White)
lightToggle.render(); // Output: Rendering a toggle switch. (Background: White)

// UI elements using the dark theme
const darkButton = new Button(darkTheme);
const darkToggle = new Toggle(darkTheme);

darkButton.render(); // Output: Rendering a button. (Background: Black)
darkToggle.render(); // Output: Rendering a toggle switch. (Background: Black)

If you want to add a new 'Blue Theme', you only need to create a new BlueTheme class. The existing Button or Toggle code doesn't need any modification. Similarly, if you want to add a new UI element like a 'Dropdown Menu', you just create a Dropdown class without touching the existing theme code.

Thus, the Bridge pattern separates the abstraction hierarchy (UI elements) from the implementation hierarchy (themes), allowing them to be extended independently.

Bridge Pattern Keywords 🔑

  • 🌉 Decouples abstraction and implementation so they can be extended independently.
  • 🔗 Uses Composition instead of inheritance.
  • 💥 Prevents the "class explosion" problem and increases flexibility.
  • ⚙️ Useful when features and implementations change at different rates or when supporting multiple platforms.

🕊️ 7. Flyweight Pattern

The Flyweight pattern is a design pattern that reduces memory usage by sharing objects to support large numbers of fine-grained objects efficiently. 'Flyweight' refers to a weight class in boxing, and as the name implies, its purpose is to make objects lightweight to minimize memory overhead.

This pattern separates an object's state into two types:

  • Intrinsic State: State managed within the object, which is immutable and can be shared across multiple contexts. It is stored within the flyweight object. (e.g., a tree's model, texture, color)
  • Extrinsic State: State that can vary for each object, managed by the client and passed to the flyweight object when needed. (e.g., a tree's position (x, y coordinates), size)

🏗️ Basic Structure

  • Flyweight: Defines the interface for objects that can be shared.
  • ConcreteFlyweight: Implements the Flyweight interface and stores the intrinsic state. This object is shared and reused.
  • FlyweightFactory: A factory that creates and manages flyweight objects. If a client requests a flyweight that already exists, it returns the existing object; otherwise, it creates a new one and stores it in a pool.
  • Client: Manages the extrinsic state and uses the FlyweightFactory to get flyweight objects to perform necessary tasks.

💻 Example: Rendering Numerous Trees in a Game

Imagine creating a game that needs to render a forest with thousands or tens of thousands of trees. Creating an object with heavy data like models and textures for each tree would consume an enormous amount of memory. The Flyweight pattern can effectively solve this problem.

First, let's define the shared tree model (TreeModel).

javascript
// Flyweight: Shared tree model
class TreeModel {
  constructor(mesh, texture) {
    // Intrinsic State (sharable, immutable)
    this.mesh = mesh; // 3D model data
    this.texture = texture; // Surface texture data
    console.log(
      `[Model Loading] Loaded ${this.mesh} and ${this.texture}. (High memory consumption)`
    );
  }

  // Method to draw on screen, receiving extrinsic state
  draw(x, y, scale) {
    console.log(
      `Drawing '${this.mesh}' tree at (${x}, ${y}) with size ${scale}.`
    );
  }
}

Now, let's create a TreeModelFactory to manage TreeModel objects.

javascript
// FlyweightFactory: Tree model factory
class TreeModelFactory {
  constructor() {
    this.models = {};
  }

  getModel(mesh, texture) {
    const key = `${mesh}-${texture}`;
    if (!this.models[key]) {
      this.models[key] = new TreeModel(mesh, texture);
    } else {
      console.log(`[Factory] Reusing existing model: ${key}`);
    }
    return this.models[key];
  }

  getModelCount() {
    return Object.keys(this.models).length;
  }
}

The client (game engine) code uses the factory to create and draw trees on the screen.

javascript
// Client: Game Engine
class Forest {
  constructor() {
    this.trees = [];
    this.factory = new TreeModelFactory();
  }

  // Plant a tree in the forest
  plantTree(x, y, scale, mesh, texture) {
    const model = this.factory.getModel(mesh, texture);
    // Store extrinsic state along with the shared model
    this.trees.push({ x, y, scale, model });
  }

  // Render the entire forest
  render() {
    this.trees.forEach(tree => {
      tree.model.draw(tree.x, tree.y, tree.scale);
    });
  }
}

// Execution
const forest = new Forest();

console.log("--- Forest Creation Start ---");
forest.plantTree(10, 20, 1.0, "Pine", "pine_texture.png");
forest.plantTree(50, 80, 1.2, "Oak", "oak_texture.png");
forest.plantTree(100, 30, 1.0, "Pine", "pine_texture.png");
forest.plantTree(200, 150, 1.1, "Pine", "pine_texture.png");
console.log("--- Forest Creation Complete ---");

console.log("
--- Forest Rendering Start ---");
forest.render();
console.log("--- Forest Rendering Complete ---");


console.log(`
Total number of tree model objects created: ${forest.factory.getModelCount()}`);

Output:

text
--- Forest Creation Start ---
[Model Loading] Loaded Pine and pine_texture.png. (High memory consumption)
[Model Loading] Loaded Oak and oak_texture.png. (High memory consumption)
[Factory] Reusing existing model: Pine-pine_texture.png
[Factory] Reusing existing model: Pine-pine_texture.png
--- Forest Creation Complete ---

--- Forest Rendering Start ---
Drawing 'Pine' tree at (10, 20) with size 1.
Drawing 'Oak' tree at (50, 80) with size 1.2.
Drawing 'Pine' tree at (100, 30) with size 1.
Drawing 'Pine' tree at (200, 150) with size 1.1.
--- Forest Rendering Complete ---

Total number of tree model objects created: 2

Although we planted 4 trees in the forest, only 2 memory-intensive TreeModel objects—'Pine' and 'Oak'—were actually created. The 'Pine' model was created once and then reused. The unique information for each tree (position, size, etc.) is managed directly by the client as extrinsic state.

In this way, the Flyweight pattern can dramatically reduce memory usage by reusing objects that have a sharable state (intrinsic state).

🤔 What does 'virtual instance' mean?

The phrase "provides multiple virtual instances to save memory" captures the essence of the Flyweight pattern, but the term 'virtual instance' can be confusing.

Let's use a 'rubber stamp' analogy for an easier understanding.

Imagine you have to stamp a 'Pine Tree' image on thousands of pages of a document.

  • Without Flyweight Pattern: To stamp 1,000 trees, you would need 1,000 actual 'Pine Tree' stamps. This would require a huge amount of space (memory) to store the stamps.
  • With Flyweight Pattern:
    1. You create only one 'Pine Tree' rubber stamp. This stamp is the flyweight object, and it holds the intrinsic state of the 'Pine Tree' shape.
    2. Every time you need to stamp a tree, you reuse this single 'Pine Tree' stamp.
    3. Instead, the information that changes for each tree—where to stamp it (x, y coordinates) and at what size—is provided from the outside at the moment of stamping. This is the extrinsic state.

Here, a 'virtual instance' refers to each 'Pine Tree' image we see on the pages. In actual memory, there is only one object (the stamp) with the 'Pine Tree' shape, but the combination of [1 shared actual object] + [different external states (coordinates, size)] makes it appear as if there are multiple independent objects.

In the game example, only 2 TreeModel objects ('Pine', 'Oak') were created, but on the screen, we see 4 trees, which are equivalent to 4 'virtual instances'.

Flyweight Pattern Keywords 🔑

  • 💾 Reduces memory usage through object sharing and providing virtual instances.
  • ⚖️ Separates an object's state into intrinsic state (sharable) and extrinsic state (non-sharable).
  • 🏭 Uses a factory to manage shared objects.
  • 🎮 Extremely useful when you need to handle a large number of objects efficiently, such as in games or document editors.