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.
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:
type, meshData, textureData). This is the "heavy" part that is stored inside the Flyweight object.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.
draw(canvas, x, y)).Flyweight interface and stores the shared, intrinsic state. You will have very few instances of this class.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.
Benefits:
Drawbacks: