Skip to Content
Course content

295: The Flyweight Pattern

Click on the "Edit" button in the top corner of the screen to edit your slide content.

A few years ago, I was helping a buddy who was building a 2D strategy game. He had a map with thousands of individual forest tiles. Each tile was its own object, and each object held a reference to a high-res texture, a wind-animation script, and the X/Y coordinates of where it sat on the map. It worked fine for a small village, but as soon as he tried to render a full kingdom, the JVM started choking. He was staring at a massive OutOfMemoryError and his first instinct was to just bump the heap size to 8GB. I told him that was like trying to fix a leaky faucet by buying a bigger sink; we needed to change how he was storing his data.

The problem was that he was duplicating the "heavy" data—the texture and the animation—thousands of times, even though every "Pine Tree" tile used the exact same assets. This is exactly where the Flyweight pattern comes in. The core idea is to stop creating redundant objects and instead share a single instance of the heavy data across many contexts.

Splitting Intrinsic and Extrinsic State

To make this work, you have to divide your object's data into two categories: intrinsic and extrinsic. This is the part that usually trips people up, but it's simpler than it sounds.

Intrinsic state is the data that is constant and independent of the object's context. In our forest example, the texture and the tree species are intrinsic. Every "Oak Tree" looks like an "Oak Tree" regardless of where it is on the map. Extrinsic state is the data that varies. The X and Y coordinates are extrinsic; they change depending on where the tree is placed.

By moving the intrinsic state into a separate "Flyweight" object, you can have 10,000 trees on your map but only two or three actual Flyweight objects in memory (one for Oak, one for Pine, one for Birch).

Managing the Shared Pool with a Factory

You can't just let the client code instantiate these shared objects whenever they feel like it, or you'll end up right back where we started with memory bloat. You need a Factory to act as the gatekeeper. The factory checks if a Flyweight for a specific type already exists in a cache (usually a HashMap). If it does, it returns the existing one; if not, it creates it, stores it, and then returns it.

public class TreeType {
    private final String name;
    private final String color;
    private final String textureData; // Imagine this is a huge byte array

    public TreeType(String name, String color, String textureData) {
        this.name = name;
        this.color = color;
        this.textureData = textureData;
    }

    public void draw(int x, int y) {
        System.out.println("Drawing " + name + " at " + x + "," + y + " using " + textureData);
    }
}

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

    public static TreeType getTreeType(String name, String color, String texture) {
        if (!treeTypes.containsKey(name)) {
            System.out.println("Creating new TreeType: " + name);
            treeTypes.put(name, new TreeType(name, color, texture));
        }
        return treeTypes.get(name);
    }
}

Wait, you might be wondering: "Where is the Tree object then?" That's the trick. The "Tree" object becomes a very thin wrapper that only holds the extrinsic state and a reference to the Flyweight.

Injecting Context into the Flyweight

Because the Flyweight object doesn't know its own coordinates, you have to pass that information back into it when it's time to actually do something, like rendering. I call this "injecting the context." Instead of the Flyweight saying "I am at X,Y," the thin wrapper says "Hey Flyweight, render yourself at my X,Y."

public class Tree {
    private int x, y;
    private TreeType type; // This is the shared Flyweight reference

    public Tree(int x, int y, TreeType type) {
        this.x = x;
        this.y = y;
        this.type = type;
    }

    public void display() {
        type.draw(x, y);
    }
}

If you've ever used Integer.valueOf(int) in Java, you've already interacted with a Flyweight. Java caches integers from -128 to 127 because those are used so frequently. Instead of creating a new object every time you need the number 10, Java just gives you a reference to the one it already has cached. It's a small optimization, but when multiplied by millions of operations, it's a lifesaver.




📋 Practical Task

Building a Memory-Efficient Particle System

You are building a visual effects system for a game that needs to render 100,000 "Spark" particles on screen. Each particle has a color, a shape (e.g., "Circle", "Square", "Star"), and a size. These three properties are the same for all particles of a certain effect type. However, each particle has its own currentX and currentY positions.

Your Task:

  • Create a ParticleType class (the Flyweight) that stores the color, shape, and size.
  • Implement a ParticleFactory that ensures only one instance of a ParticleType exists for any unique combination of color, shape, and size.
  • Create a Particle class (the context) that stores the currentX and currentY and holds a reference to its ParticleType.
  • In your main method, simulate the creation of 100,000 particles, but only use 3 distinct types (e.g., "Red Circle Small", "Blue Square Large", "Yellow Star Medium").
  • Print the total number of ParticleType objects created to prove that you've only instantiated 3, despite having 100,000 Particle objects.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.