Low-level design3 min
Flyweight Pattern
Type: Structural Pattern
Relevance: Low/Medium. Very niche. Primarily used in game development or memory-constrained environments.
The Flyweight pattern minimizes memory usage by sharing as much data as possible with similar objects, rather than keeping all data in each object.
Real-Life Analogy
Think of a Forest in a Video Game.
A forest might have 1,000,000 trees. If every single Tree object stores a 30MB 3D mesh and a 10MB texture map, your game will consume 40 Terabytes of RAM and instantly crash.
However, there are really only 3 types of trees in the forest (Oak, Pine, Birch).
Instead of duplicating the 3D mesh 1,000,000 times, you store the mesh and texture once (the Flyweight), and the 1,000,000 Tree objects only store their unique coordinates (X, Y) and a reference to the shared Flyweight.
The Problem (Intrinsic vs Extrinsic State)
The pattern relies on separating an object's state into two parts:
- Intrinsic State: The constant data stored inside the object that can be shared (e.g., Tree color, texture, 3D mesh). This is the Flyweight.
- Extrinsic State: The unique data that changes per object (e.g., X/Y coordinates). This is passed to the Flyweight by the client.
How to Implement Flyweight
Extract the intrinsic state into a Flyweight class. Create a Flyweight Factory to manage a pool of existing Flyweights, ensuring they are shared and not unnecessarily duplicated.
Example in Code
// 1. The Flyweight (Intrinsic State)
// This holds the heavy, memory-consuming data.
class TreeType {
constructor(
private name: string,
private color: string,
private texture: string // Imagine this is 40MB of data
) {}
public draw(canvas: any, x: number, y: number): void {
console.log(`Drawing ${this.name} tree at (${x}, ${y}) with color ${this.color}`);
}
}
// 2. The Flyweight Factory
// Manages the pool of shared Flyweights.
class TreeFactory {
private static treeTypes: { [key: string]: TreeType } = {};
public static getTreeType(name: string, color: string, texture: string): TreeType {
const key = `${name}-${color}-${texture}`;
// If it doesn't exist, create and cache it
if (!this.treeTypes[key]) {
console.log(`Factory: Creating new TreeType (${key})`);
this.treeTypes[key] = new TreeType(name, color, texture);
}
return this.treeTypes[key];
}
}
// 3. The Context Object (Extrinsic State)
// This is very lightweight! It only holds coordinates and a reference.
class Tree {
constructor(
private x: number,
private y: number,
private type: TreeType
) {}
public draw(canvas: any): void {
this.type.draw(canvas, this.x, this.y);
}
}
// Client Code (A Forest)
const forest: Tree[] = [];
function plantTree(x: number, y: number, name: string, color: string, texture: string) {
// The Factory ensures we reuse the TreeType if it already exists!
const type = TreeFactory.getTreeType(name, color, texture);
const tree = new Tree(x, y, type);
forest.push(tree);
}
// Planting 5 trees, but only 2 heavy TreeTypes are created in memory!
plantTree(1, 2, "Oak", "Green", "oak.png");
plantTree(5, 3, "Oak", "Green", "oak.png");
plantTree(8, 9, "Oak", "Green", "oak.png");
plantTree(2, 4, "Pine", "DarkGreen", "pine.png");
plantTree(7, 1, "Pine", "DarkGreen", "pine.png");
Why is it Important?
- Massive Memory Savings: It can turn an application that requires gigabytes of RAM into one that requires megabytes, allowing huge amounts of objects to be spawned.