Flyweight Pattern in TypeScript — ✨ Structural Design Pattern #5
In software development, there are situations where we need to work with a large number of fine-grained objects that have similar intrinsic
properties. Creating and managing these objects individually can consume significant memory and impact performance. The Flyweight Pattern provides a solution to optimize memory usage by sharing common states across multiple objects.
🙋🏻♀️ What is the Flyweight Pattern?
The Flyweight Pattern is a structural design pattern that aims to minimize memory usage by sharing common states across multiple objects. It is particularly useful when dealing with a large number of objects that have similar intrinsic properties but can have varying extrinsic
properties.
Extrinsic vs Intrinsic Properties
- Extrinsic Property is a shared/constant state that is independent of the context.
- Intrinsic Property is a context-specific state to each individual object and can vary in different contexts or usages.
The main idea behind the Flyweight Pattern is to separate the intrinsic
state and extrinsic
state of objects. By sharing the intrinsic
state, we can significantly reduce memory usage and improve performance. The Flyweight Pattern achieves this by creating a pool or factory of flyweight
objects that can be reused and shared by multiple clients.
✨ Simple Example
Let’s consider an example of rendering a large number of monsters in a graphical game. Each monster has intrinsic
properties such as its type, texture, and color, which are the same for all instances of the same monster type. The extrinsic
properties may include the position, size, and rotation of each monster, which can vary for each instance.
In this example, we have two types of monsters: Goblin and Dragon.
type MonsterType = 'Goblin' | 'Dragon'
Each monster type has its intrinsic state, including the type, texture, and color. The extrinsic
state, such as the position, is passed to the render()
method of each monster object.
interface Monster {
render(x: number, y: number): void // x, y: extrinsic state
}
class Goblin implements Monster {
private readonly type: MonsterType
private readonly texture: string
private readonly color: string
constructor() {
this.type = 'Goblin'
this.texture = 'goblin_texture.jpg'
this.color = 'green'
}
render(x: number, y: number): void {
console.log(`Rendering a ${this.color} ${this.texture} ${this.type} at (${x}, ${y})`)
}
}
class Dragon implements Monster {
private readonly type: MonsterType
private readonly texture: string
private readonly color: string
constructor() {
this.type = 'Dragon'
this.texture = 'dragon_texture.jpg'
this.color = 'red'
}
render(x: number, y: number): void {
console.log(`Rendering a ${this.color} ${this.texture} ${this.type} at (${x}, ${y})`)
}
}
The FlyweightMonsterFactory
creates and manages the flyweight
objects.
Instead of creating a new monster object for each instance, we reuse the flyweight
objects from the factory.
class FlyweightMonsterFactory {
private cache: { [key in MonsterType]?: Monster } = {}
getMonster(type: MonsterType): Monster {
let monster = this.cache[type]
if (!monster) {
if (type === 'Goblin') monster = new Goblin()
else monster = new Dragon()
this.cache[type] = monster
}
return monster
}
}
This example demonstrates how the Flyweight Pattern can be applied to optimize memory usage when dealing with a large number of fine-grained objects.
// Client code
const monsterFactory = new MonsterFactory();
const monsters: Monster[] = [];
for (let i = 0; i < 10_000; i++) {
const type = i % 2 === 0 ? 'Goblin' : 'Dragon';
const monster = monsterFactory.getMonster(type);
monsters.push(monster);
monster.render(i, Math.random() * 10_000);
}
🧑🏻💻 Use it or Avoid it
When to use it:
- To work with a large number of fine-grained objects that have similar intrinsic properties.
- To reduce memory usage by sharing common states among multiple objects.
- To improve performance when creating and managing objects is costly.
When to avoid it:
- If the objects in your system have significant variations in their
intrinsic
properties. The benefit of memory optimization may not outweigh the complexity introduced by separatingintrinsic
andextrinsic
states. - If the objects are not frequently reused or if the memory usage reduction is negligible compared to the overhead of managing the
flyweight
objects.