Flyweight

Structural Design Patterns

Imagine you're developing a realistic video game that needs to render a dense forest. This forest could contain millions of Tree objects. If each Tree object is a standalone, heavyweight object, you'll face a massive memory problem.

Consider a simple Tree class:

public class Tree {
    // Unique to each tree
    private int x, y; 
    
    // Often shared between many trees
    private String type;         // "Banyan", "Neem", etc.
    private byte[] meshData;     // 3D model data (very large)
    private byte[] textureData;  // Bark/leaf texture data (very large)
    
    public Tree(int x, int y, String type, byte[] mesh, byte[] texture) { /* ... */ }
}

If you create one million Tree objects, and the mesh and texture data for each type of tree (e.g., an "Banyan") is several megabytes, you would consume terabytes of RAM. The application would crash before it even started.

How can you render a vast number of similar objects without running out of memory? The key insight is that much of the data (type, meshData, textureData) is identical for thousands of trees. This shared, heavy data is the target for our optimization.

Sharing to Support Large Numbers

The Flyweight is a structural design pattern that lets you fit more objects into the available amount of RAM by sharing common parts of state between multiple objects instead of keeping all of the data in each object.

To achieve this, the Flyweight pattern separates the state of an object into two parts:

  1. Intrinsic State: The data that is immutable and shared among many objects (e.g., the type, meshData, textureData). This is the "heavy" part that is stored inside the Flyweight object.
  2. Extrinsic State: The data that is unique to each object and cannot be shared (e.g., the x and y coordinates, size). This "contextual" data is stored outside the Flyweight object and is passed in by the client when an operation is needed.

The pattern's goal is to create as few Flyweight objects as possible to store the intrinsic state, and then reuse and share them across many "context" objects that hold the extrinsic state.

The Participants

  1. Flyweight: The interface through which clients interact with the shared objects. It typically has methods that accept the extrinsic state as parameters (e.g., draw(canvas, x, y)).
  2. Concrete Flyweight: The class that implements the Flyweight interface and stores the shared, intrinsic state. You will have very few instances of this class.
  3. Flyweight Factory: A crucial component that manages a pool (or cache) of Flyweight objects. The client asks the factory for a Flyweight for a given type. The factory checks its pool; if a Flyweight for that type already exists, it returns the shared instance. If not, it creates a new one, stores it in the pool, and then returns it.
  4. Client/Context: The client maintains the extrinsic state and passes it to the Flyweight's methods.

Example: Rendering a Forest

Let's refactor our Tree example to use the Flyweight pattern.

Step 1: The Flyweight Class (TreeType)

This class stores the shared, heavy, intrinsic state.

public class TreeType {
    // Intrinsic, shared state
    private final String name;
    private final String color;
    private final byte[] textureData;

    public TreeType(String name, String color, byte[] textureData) { /* ... constructor ... */ }

    // The operation uses extrinsic state passed in by the client
    public void draw(Canvas canvas, int x, int y) {
        System.out.println("Drawing a " + name + " at (" + x + ", " + y + ")");
    }
}

Step 2: The Flyweight Factory (TreeFactory)

This factory ensures that TreeType objects are shared.

import java.util.HashMap;
import java.util.Map;

public class TreeFactory {
    private static final Map<String, TreeType> treeTypes = new HashMap<>();

    public static TreeType getTreeType(String name, String color, byte[] texture) {
        String key = name + "_" + color; // Create a key for the cache
        TreeType result = treeTypes.get(key);

        if (result == null) {
            // If the type doesn't exist, create it and add to the cache.
            result = new TreeType(name, color, texture);
            treeTypes.put(key, result);
            System.out.println("Creating new flyweight for type: " + key);
        }
        return result;
    }
}

Step 3: The "Context" Class (Tree)

This lightweight class holds the unique, extrinsic state and a reference to a shared Flyweight object.

public class Tree {
    // Extrinsic, unique state
    private final int x;
    private final int y;
    
    // A reference to the shared Flyweight
    private final TreeType type;

    public Tree(int x, int y, TreeType type) { /* ... constructor ... */ }

    public void draw(Canvas canvas) {
        // Delegate the drawing to the flyweight, passing in the extrinsic state.
        type.draw(canvas, this.x, this.y);
    }
}

Step 4: The Client (Forest)

The client now creates many lightweight Tree objects, but only a few heavyweight TreeType flyweights.

public class Forest {
    private final List<Tree> trees = new ArrayList<>();

    public void plantTree(int x, int y, String name, String color, byte[] texture) {
        TreeType type = TreeFactory.getTreeType(name, color, texture);
        Tree tree = new Tree(x, y, type);
        trees.add(tree);
    }
    // ...
}

If you plant a million "Banyan" trees, you will have one million lightweight Tree objects but only one shared TreeType object for "Banyan", resulting in massive memory savings.

Interview Focus: Analysis and Trade-offs

Benefits:

  • Drastic Memory Reduction: This is the primary reason to use the pattern. The memory savings can be enormous if the application has a large number of similar objects.

Drawbacks:

  • Increased Complexity: You must separate your object's state into intrinsic and extrinsic parts and introduce a factory, which can make the code more complex.
  • CPU vs. RAM Trade-off: While you save RAM, there can be a slight CPU overhead. The client has to compute or retrieve and pass the extrinsic state to the flyweight on every method call. This is almost always a worthwhile trade-off when memory is a concern.

How Flyweight Relates to Other Concepts

  • Flyweight vs. Singleton: This is a common point of confusion.
    • A Singleton enforces that there is only one instance of a class in the entire system.
    • A Flyweight allows for several shared instances, each representing a different configuration of intrinsic state (e.g., one instance for "Banyan" trees, another for "Pine" trees). The Flyweight Factory itself, however, is often implemented as a Singleton.
  • Immutable Flyweights: The Flyweight objects that store intrinsic state must be immutable. If a client could change the shared state (e.g., change an Banyan's texture), it would instantly and incorrectly affect all other objects sharing that flyweight.
  • Real-World Examples:
    • Character Rendering: In a word processor, instead of every character on the page being a unique object, there is one flyweight object for each unique character/font/style combination (e.g., 'a' in Arial 12pt). The character's position on the page is the extrinsic state.
    • Database Connection Pools: The connection objects in a pool can be considered flyweights. They are shared and reused by different parts of the application. The specific SQL query you run (the extrinsic state) is passed to the connection object when you use it.