Flyweight Pattern in TypeScript — ✨ Structural Design Pattern #5

365kim
3 min readJun 26, 2023

--

Photo by Victor Freitas on Unsplash

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 separating intrinsic and extrinsic 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.

--

--

365kim

Web Front-End Developer who believes Every Day Counts!