Flyweight Pattern is a structural design pattern that minimizes memory usage and improves performance by sharing objects. Simply put, it embodies the idea of "sharing metadata".
Imagine going to a library to borrow books: if every book were purchased separately by different people, the library would need to store thousands of identical copies. But in reality, the library only needs to store one copy of "Python for Beginners", and everyone who wants to read it can borrow the same copy. The Flyweight Pattern is like this "library", managing shareable objects.
Core Idea
The Flyweight Pattern divides object state into two types:
- Intrinsic State: The unchanging, shareable part
- Extrinsic State: The changing, unshareable part
By sharing intrinsic state, it avoids creating large numbers of similar objects, thereby saving system resources.
Why We Need the Flyweight Pattern
Problem Scenario
Suppose we are developing a word processor that needs to render characters in a document. If each character creates an independent object:
Example
# Bad implementation: each character is an independent object
class Character:
def __init__(self, char, font, size, color):
self.char = char # character
self.font = font # font
self.size = size # size
self.color = color # color
def render(self, position):
print(f"Render character '{self.char}' at position {position}")
# Usage example
char_a = Character('A', 'SimSun', 12, 'black')
char_b = Character('B', 'SimSun', 12, 'black')
char_a_another = Character('A', 'SimSun', 12, 'black') # Duplicate creation of same 'A'
Problems with this implementation:
- Memory waste: identical characters are created repeatedly
- Low performance: overhead of creating and destroying large numbers of objects
- Difficult to maintain: explosive growth in object count
Solution
Using the Flyweight Pattern, we can:
- Share identical character objects
- Store only one copy of intrinsic state (the character itself)
- Pass extrinsic state (position) at render time
Implementation of the Flyweight Pattern
Basic Structure
Let's implement the Flyweight Pattern using the word processor example:
Example
from typing import Dict
# Flyweight class - stores intrinsic state
class CharacterFlyweight:
def __init__(self, char: str, font: str, size: int, color: str):
self.char = char # intrinsic state: character content
self.font = font # intrinsic state: font
self.size = size # intrinsic state: size
self.color = color # intrinsic state: color
def render(self, position: tuple):
"""Render character, position is extrinsic state"""
x, y = position
print(f"Render at position ({x}, {y}): character '{self.char}' "
f"[font:{self.font}, size:{self.size}, color:{self.color}]")
# Flyweight factory - manages shared objects
class CharacterFactory:
_characters: Dict[str, CharacterFlyweight] = {}
@classmethod
def get_character(cls, char: str, font: str, size: int, color: str) -> CharacterFlyweight:
# Create unique identifier key for the object
key = f"{char}_{font}_{size}_{color}"
# If object doesn't exist, create and cache it
if key not in cls._characters:
cls._characters = CharacterFlyweight(char, font, size, color)
print(f"Create new character object: {key}")
else:
print(f"Reuse existing character object: {key}")
return cls._characters
# Client class - uses flyweight objects
class TextDocument:
def __init__(self):
self.characters = [] # stores character and position information
def add_character(self, char: str, font: str, size: int, color: str, position: tuple):
# Get flyweight object from factory
character = CharacterFactory.get_character(char, font, size, color)
# Store character object and extrinsic state (position)
self.characters.append((character, position))
def render(self):
print("\n=== Start rendering document ===")
for character, position in self.characters:
character.render(position)
print("=== Document rendering complete ===\n")
Usage Example
Example
# Create document
document = TextDocument()
# Add characters to document
document.add_character('H', 'Arial', 12, 'black', (0, 0))
document.add_character('e', 'Arial', 12, 'black', (1, 0))
document.add_character('l', 'Arial', 12, 'black', (2, 0))
document.add_character('l', 'Arial', 12, 'black', (3, 0)) # Reuse 'l'
document.add_character('o', 'Arial', 12, 'black', (4, 0))
document.add_character('!', 'Arial', 12, 'red', (5, 0)) # Different color, create new object
document.add_character('H', 'Arial', 12, 'black', (0, 1)) # Reuse 'H'
# Render document
document.render()
# View number of objects in factory
print(f"Total character objects created in factory: {len(CharacterFactory._characters)}")
Output:
Create new character object: H_Arial_12_black
Create new character object: e_Arial_12_black
Create new character object: l_Arial_12_black
Reuse existing character object: l_Arial_12_black
Create new character object: o_Arial_12_black
Create new character object: !_Arial_12_red
Reuse existing character object: H_Arial_12_black
=== Start rendering document ===
Render at position (0, 0): character 'H' [font:Arial, size:12, color:black]
Render at position (1, 0): character 'e' [font:Arial, size:12, color:black]
Render at position (2, 0): character 'l' [font:Arial, size:12, color:black]
Render at position (3, 0): character 'l' [font:Arial, size:12, color:black]
Render at position (4, 0): character 'o' [font:Arial, size:12, color:black]
Render at position (5, 0): character '!' [font:Arial, size:12, color:red]
Render at position (0, 1): character 'H' [font:Arial, size:12, color:black]
=== Document rendering complete ===
Total character objects created in factory: 6
Core Components of the Flyweight Pattern
1. Flyweight (Flyweight Interface or Abstract Class)
Defines the interface for flyweight objects, typically containing methods that operate on extrinsic state.
2. ConcreteFlyweight (Concrete Flyweight Class)
Implements the flyweight interface, storing intrinsic state. Intrinsic state must be immutable.
3. FlyweightFactory (Flyweight Factory)
Creates and manages flyweight objects, ensuring proper sharing of flyweight objects.
4. Client
Maintains extrinsic state, requesting flyweight objects from the flyweight factory when needed.
More Complex Example: Application in Game Development
Let's look at a practical example in game development - a tree rendering system:
Example
from typing import Dict, List
from dataclasses import dataclass
@dataclass
class TreeType:
"""Flyweight class - tree type (intrinsic state)"""
name: str # tree species name
texture: str # texture file
color: str # base color
def render(self, x: int, y: int, height: int):
"""Render tree, position and height are extrinsic state"""
print(f"Render {self.name} tree at ({x}, {y}), height {height}m "
f"[texture:{self.texture}, color:{self.color}]")
class TreeFactory:
"""Flyweight factory - manages tree types"""
_tree_types: Dict[str, TreeType] = {}
@classmethod
def get_tree_type(cls, name: str, texture: str, color: str) -> TreeType:
key = f"{name}_{texture}_{color}"
if key not in cls._tree_types:
cls._tree_types = TreeType(name, texture, color)
print(f"Create new tree type: {name}")
return cls._tree_types
@classmethod
def list_tree_types(cls):
print(f"\nCurrently have {len(cls._tree_types)} tree types:")
for tree_type in cls._tree_types.values():
print(f" - {tree_type.name}")
class Tree:
"""Tree object - contains flyweight reference and extrinsic state"""
def __init__(self, x: int, y: int, height: int, tree_type: TreeType):
self.x = x # extrinsic state: X coordinate
self.y = y # extrinsic state: Y coordinate
self.height = height # extrinsic state: height
self.tree_type = tree_type # flyweight object reference
def render(self):
self.tree_type.render(self.x, self.y, self.height)
class Forest:
"""Forest - client class"""
def __init__(self):
self.trees: List = []
def plant_tree(self, x: int, y: int, height: int,
name: str, texture: str, color: str):
tree_type = TreeFactory.get_tree_type(name, texture, color)
tree = Tree(x, y, height, tree_type)
self.trees.append(tree)
def render(self):
print("\n=== Start rendering forest ===")
for tree in self.trees:
tree.render()
print("=== Forest rendering complete ===")
# Usage example
forest = Forest()
# Plant trees - same type of trees will share flyweight objects
forest.plant_tree(10, 20, 15, "Pine", "pine_texture.png", "dark green")
forest.plant_tree(30, 40, 12, "Pine", "pine_texture.png", "dark green") # Reuse pine type
forest.plant_tree(50, 60, 18, "Oak", "oak_texture.png", "light green")
forest.plant_tree(70, 80, 20, "Pine", "pine_texture.png", "dark green") # Reuse again
forest.plant_tree(90, 100, 16, "Maple", "maple_texture.png", "red")
# Render forest
forest.render()
# View tree type statistics
TreeFactory.list_tree_types()
Advantages and Disadvantages of the Flyweight Pattern
Advantages
- Significantly reduces memory usage: dramatically lowers memory footprint by sharing similar objects
- Improves performance: reduces overhead of object creation and garbage collection
- Code reuse: identical object logic only needs to be implemented once
- Easy to extend: adding new flyweight types won't affect existing code
Disadvantages
- Increases complexity: need to distinguish between intrinsic and extrinsic state
- Thread safety issues: shared objects require additional handling in multi-threaded environments
- May introduce bugs: if intrinsic state is modified incorrectly, it affects all users
- Not applicable to all scenarios: only effective when objects are truly shareable
Applicable Scenarios
Scenarios Suitable for the Flyweight Pattern
- Large numbers of similar objects: when the system needs to create large numbers of similar objects
- Memory-sensitive applications: mobile devices, embedded systems, and other memory-constrained environments
- Caching systems: when caching and reusing objects is needed
- Game development: rendering large numbers of identical game objects
- Document processing: word processors, spreadsheet processing, etc.
Scenarios Not Suitable for the Flyweight Pattern
- Objects with large differences: if each object has unique state
- Complex extrinsic state: if managing extrinsic state is more complex than object creation
- Low performance requirements: in scenarios with sufficient memory and low performance requirements
Practical Exercises
Exercise 1: Improve the Character Rendering System
Try to improve our previous character rendering system by adding support for bold, italic, and other styles:
Example
# Your improved code here
class AdvancedCharacterFlyweight:
# Add support for bold, italic, underline
pass
# Test your implementation
def test_advanced_system():
# Create document with different styles
pass
Exercise 2: Implement an Icon Management System
Design an icon management system where the same icon shares the same object when displayed at different positions:
Example
class IconFlyweight:
# Store icon file path, size, and other intrinsic state
pass
class IconFactory:
# Manage icon flyweight objects
pass
class Application:
# Display icons at different positions in the interface
pass
Summary
The Flyweight Pattern is a powerful optimization technique that reduces resource consumption by sharing objects. Key points:
- Distinguish states: clearly differentiate intrinsic state (shareable) and extrinsic state (unshareable)
- Use factory: manage flyweight object creation and sharing through a factory class
- Weigh trade-offs: find balance between memory savings and code complexity
- Applicable scenarios: mainly used in scenarios with large numbers of similar objects
YouTip