This week we’ll continue our exploration of design patterns and look at a structural pattern, the Flyweight pattern.
The Gang of Four book has a very simple definition for the Flyweight pattern:
“Use sharing to support large numbers of fine-grained objects efficiently.”
Understanding the Flyweight Pattern
Let’s say that we have an application that needs a large number of objects of the same type. If each of those objects has a large amount of internal state (instance fields), then creating many of them will use a proportionally large amount of memory.
If we look carefully at each object’s state, we might find that some of the fields are common, and could potentially be shared among the set of objects. We would then move the common shareable state into a flyweight object.
The internal state of an object can be split into two parts:
- Intrinsic (shared) data: This is stored in the flyweight object. The data is independent of the flyweight’s context and can be shared. We can replace all of the objects having the same intrinsic data with a single immutable flyweight object.
- Extrinsic (unshared) data: This is data that depends on the flyweight’s context and cannot be shared. Client code stores and computes extrinsic data, and passes that data to the flyweight when it is needed.
Because the same flyweight object can be used in different contexts, we must ensure that its state can’t be modified. A flyweight should be immutable and initialize its state in a constructor. It shouldn’t expose any public fields or setters to other objects.
The Flyweight pattern can usually be recognized by an object creation method returning cached objects instead of creating new objects.
An example of this from the standard Java API libraries is the Integer
class. Internally it contains a cache of values in the range -128
to 127
. These cached values are returned by the Integer.valueOf(int)
method. The Byte
, Short
and Long
classes have the same behaviour. The Boolean
class has two flyweight objects FALSE
and TRUE
. The BigDecimal
and BigInteger
classes have three flyweight objects each: ZERO
, ONE
and TEN
.
Flyweight Pattern: Design Example
Let’s go back to our first person shooter game. We’ve already created many types of monsters, such as zombies, vampires, werewolves and the like. We occasionally create hordes of these monsters to attack players. Hordes of monsters can use a lot of memory, so we’d like a way to share common state between monsters of particular types.
If we think about the internal state of each monster, we would need to model its position on the map, its current damage/health, the set of images that we’d use to draw the monster, and other relevant data.
public class Monster { // instance fields to model state private int x, y, z; // map position/co-ordinates private int damage; // damage/health value private SpriteSet sprites; // images to draw the monster private String typeName; // monster type name // other code }
Flyweight Class
Each monster will have its own position and damage/health, but all monsters of the same type (Zombie
, Vampire
, Werewolf
, etc.) will contain duplicate data (type name, images, sprites, etc.). We can store these duplicate values inside separate flyweight objects, e.g. a MonsterType
class. We will then reference a single shared flyweight object, instead of storing the same duplicate data in the hordes of Monster
objects.
Let’s move the shared data out of the Monster
class and into the new MonsterType
class.
// Contains state that is shared between Monsters public class MonsterType { // instance fields private String typeName; // monster type name private SpriteSet sprites; // images used to draw the monster // constructors and other code }
Flyweight Factory
We usually create a factory method that manages a pool of existing flyweight objects. When a client requests a flyweight object for the first time, the factory instantiates the flyweight, initializes it with the required intrinsic data, adds it to the pool, and then returns it to the client. With subsequent requests, the factory retrieves the existing flyweight from the pool and returns it to the client.
The following is code for a MonsterType
factory that instantiates and caches the flyweight objects for the different monsters:
import java.util.Map; import java.util.HashMap; // The factory creates and pools flyweight objects public class MonsterTypeFactory { private static Map cache = new HashMap<>(); public static MonsterType getMonsterType(String key) { // If key is in the cache, return appropriate flyweight. if (cache.containsKey(key)) { return cache.get(key); } // If the key is not in the cache, create the // flyweight, put it into the cache, and return it. MonsterType monsterType; switch (key) { case "Vampire" : monsterType = new MonsterType("Vampire", Vampire.SPRITE_SET); break; case "Zombie" : monsterType = new MonsterType("Zombie", Zombie.SPRITE_SET); break; default: throw new IllegalArgumentException("Unsupported monster type."); } cache.put(key, monsterType); return monsterType; } }
This factory looks very much like the CreatureFactory
we created in the earlier post on the Simple Factory design pattern. The difference here is that we also cache the objects.
Context Class
The Flyweight pattern suggests that we shouldn’t store the extrinsic state inside an object, but rather pass the state to any methods that need it. We will then need a much smaller number of objects since they only differ in the intrinsic state (the flyweight object). The intrinsic state generally has fewer variations than the extrinsic state.
public abstract class Monster { // Shared intrinsic state stored in the flyweight private MonsterType type; // Unique extrinsic state is passed in by client code public abstract void move(int newX ,int newY, int newZ); }
An alternative solution is to create a separate context class that stores both the extrinsic state and a reference to the flyweight object:
// Context class contains both intrinsic and extrinsic state public class Monster { // instance fields for extrinsic state private int x, y, z; // map position/co-ordinates private int damage; // damage/health value // reference to intrinsic flyweight state private MonsterType type; public void move(int newX ,int newY, int newZ) { x = newX; y = newY; z = newZ; // relevant code } // other code }
We will still need as many of these contextual objects as we had originally. But the objects are much smaller than before. The memory-hungry fields have been moved to just a few flyweight objects. Now if we create a hundred Monster
objects, they will all reuse a single MonsterType
flyweight object, instead of having a hundred extra copies of duplicated data in memory.
Client Code
The client code is responsible for maintaining and changing the state of the individual objects. The client calculates the extrinsic state (position, movement, health, etc.) of the monsters, and passes it to the relevant methods.
A code snippet follows:
// in client code Monster monster[] = new Monster[] { new Monster(MonsterTypeFactory.getMonsterType("Vampire") ), new Monster(MonsterTypeFactory.getMonsterType("Werewolf")), new Monster(MonsterTypeFactory.getMonsterType("Zombie") ) }; // client calculates new positions monster[0].move(50, 30, 20); monster[1].move(30, 20, 0); monster[2].move(10, 10, 0); // more code...
Pros and Cons of the Flyweight Pattern
The Flyweight pattern isn’t a very commonly used pattern nowadays because computers have become more powerful and RAM has become vastly cheaper. But when we’re dealing with large-scale systems, it will help minimise memory usage and save system resources.
What’s Next?
In the weeks ahead, we’ll continue examining some of the more useful design patterns. Stay tuned!
As always, please share your comments and questions.