Flyweight Pattern
The Flyweight pattern is a structural design pattern that aims to minimize memory usage by sharing as much data as possible with similar objects. It’s particularly useful when dealing with a large number of objects that have some shared state.
Each object consists of two types of data/state: intrinsic state
and extrinsic state
. The intrinsic state represents data that remains constant for each object, while the extrinsic state represents data that changes with each object. The Flyweight pattern suggests only storing the intrinsic state (memory heavy fields) of an object within the object, the extrinsic state can be passed to the methods which rely on it. Since most of our objects have a shared state, we would have a lesser number of objects using the flyweight pattern.
The objects which store the intrinsic state are called
flyweight
.
Structure
The Flyweight pattern consists of the following key components:
- Flyweight: is an interface that declares methods to get and set extrinsic state
- Concrete Flyweight: concrete implementations of the Flyweight interface and stores intrinsic state
- Flyweight Factory: manages flyweights objects. The clients pass some intrinsic state to the factory for a flyweight and the factory either returns a matched flyweight or creates a new one
- Context: manages and stores the extrinsic state related to flyweights, and maintains references to flyweights
- Client: uses flyweight objects using factory
When to use the Flyweight Pattern?
- When your program uses a huge number of objects that share most of the data
- When the number of objects cannot be stored in the memory/RAM
Implementation Example
The code below demonstrates the implementation of the Flyweight design pattern in Java.
This code demonstrates the Flyweight design pattern in a simple map application. We use a TerrainFactory to manage and reuse TerrainTile objects (the flyweights) representing different terrain types (grass, water, mountain). The MapCell class acts as a context, storing the position and referencing the appropriate terrain tile. The main Solution class creates a 5x5 grid of map cells, assigning terrain types based on cell positions. It then renders the map, with each cell delegating the rendering to its terrain tile. The Flyweight pattern is evident in how multiple MapCell objects share the same TerrainTile instances, reducing memory usage. This is particularly useful for larger maps where the memory savings would be more significant.
import java.util.HashMap;
import java.util.Map;
// Client class
public class Solution {
public static void main(String[] args) {
MapCell[][] map = new MapCell[5][5];
// Initialize map
for (int i = 0; i < 5; i++) {
for (int j = 0; j < 5; j++) {
String terrainType;
if (i == j) {
terrainType = "mountain";
} else if (i > j) {
terrainType = "water";
} else {
terrainType = "grass";
}
map[i][j] = new MapCell(i, j, terrainType);
}
}
// Render map
for (int i = 0; i < 5; i++) {
for (int j = 0; j < 5; j++) {
map[i][j].render();
}
}
System.out.println("\nTotal unique terrain tiles: " + TerrainFactory.getTileCount());
}
}
// Flyweight interface
interface MapTile {
void render(int x, int y);
}
// Concrete Flyweight
class TerrainTile implements MapTile {
private final String terrain;
private final int[] color;
public TerrainTile(String terrain, int[] color) {
this.terrain = terrain;
this.color = color;
}
public void render(int x, int y) {
System.out.printf("Rendering %s tile at (%d, %d) with color RGB(%d, %d, %d)%n", terrain, x, y, color[0], color[1], color[2]);
}
}
// Flyweight Factory
class TerrainFactory {
private static final Map<String, MapTile> terrainTiles = new HashMap<>();
public static MapTile getTerrain(String terrain) {
MapTile tile = terrainTiles.get(terrain);
if (tile == null) {
switch (terrain) {
case "grass":
tile = new TerrainTile("grass", new int[]{0, 255, 0});
break;
case "water":
tile = new TerrainTile("water", new int[]{0, 0, 255});
break;
case "mountain":
tile = new TerrainTile("mountain", new int[]{139, 69, 19});
break;
default:
tile = new TerrainTile("unknown", new int[]{128, 128, 128});
}
terrainTiles.put(terrain, tile);
}
return tile;
}
public static int getTileCount() {
return terrainTiles.size();
}
}
// Context class that uses the flyweight
class MapCell {
private final int x;
private final int y;
private final MapTile terrain;
public MapCell(int x, int y, String terrainType) {
this.x = x;
this.y = y;
this.terrain = TerrainFactory.getTerrain(terrainType);
}
public void render() {
terrain.render(x, y);
}
}
The output of the following program will be:
Rendering mountain tile at (0, 0) with color RGB(139, 69, 19)
Rendering grass tile at (0, 1) with color RGB(0, 255, 0)
Rendering grass tile at (0, 2) with color RGB(0, 255, 0)
Rendering grass tile at (0, 3) with color RGB(0, 255, 0)
Rendering grass tile at (0, 4) with color RGB(0, 255, 0)
Rendering water tile at (1, 0) with color RGB(0, 0, 255)
Rendering mountain tile at (1, 1) with color RGB(139, 69, 19)
Rendering grass tile at (1, 2) with color RGB(0, 255, 0)
Rendering grass tile at (1, 3) with color RGB(0, 255, 0)
Rendering grass tile at (1, 4) with color RGB(0, 255, 0)
Rendering water tile at (2, 0) with color RGB(0, 0, 255)
Rendering water tile at (2, 1) with color RGB(0, 0, 255)
Rendering mountain tile at (2, 2) with color RGB(139, 69, 19)
Rendering grass tile at (2, 3) with color RGB(0, 255, 0)
Rendering grass tile at (2, 4) with color RGB(0, 255, 0)
Rendering water tile at (3, 0) with color RGB(0, 0, 255)
Rendering water tile at (3, 1) with color RGB(0, 0, 255)
Rendering water tile at (3, 2) with color RGB(0, 0, 255)
Rendering mountain tile at (3, 3) with color RGB(139, 69, 19)
Rendering grass tile at (3, 4) with color RGB(0, 255, 0)
Rendering water tile at (4, 0) with color RGB(0, 0, 255)
Rendering water tile at (4, 1) with color RGB(0, 0, 255)
Rendering water tile at (4, 2) with color RGB(0, 0, 255)
Rendering water tile at (4, 3) with color RGB(0, 0, 255)
Rendering mountain tile at (4, 4) with color RGB(139, 69, 19)
Total unique terrain tiles: 3
Enjoyed the read? Give it a thumbs up 👍🏻!