Flyweight Pattern¶
Definition¶
The Flyweight Pattern uses sharing to support large numbers of fine-grained objects efficiently.
The trick is splitting an object's state in two:
- Intrinsic state, shared, immutable, context-free (a glyph's font and outline, a tree's texture). Stored inside the flyweight.
- Extrinsic state, unique per use (a glyph's position on the page, a tree's coordinates). Passed in by the client at call time.
When to Use?¶
- You need an enormous number of similar objects and memory is the bottleneck.
- Most of each object's state can be made extrinsic.
- Object identity does not matter to the client, two "identical" objects being the same instance must be acceptable.
Use-case Examples (Real-world Applications)¶
- Text editors sharing one glyph object per character, not per occurrence
- Game engines sharing a mesh/texture across thousands of rendered instances
- Java's
Integer.valueOf()cache and internedStringliterals
Structure¶
Example¶
The flyweight (TreeType) holds only the heavy, shared data. The factory
guarantees sharing, so clients must never construct a TreeType directly. A
million trees end up sharing one instance.
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class TreeType {
private final String name;
private final String color;
private final String texture; // stands in for a few megabytes of pixels
TreeType(String name, String color, String texture) {
this.name = name;
this.color = color;
this.texture = texture;
}
// x and y are extrinsic: supplied per call, never stored.
public void draw(int x, int y) {
System.out.println("Drawing " + color + " " + name + " at (" + x + ", " + y + ")");
}
}
public class TreeTypeFactory {
private static final Map<String, TreeType> pool = new HashMap<>();
public static TreeType get(String name, String color, String texture) {
return pool.computeIfAbsent(
name + "|" + color + "|" + texture,
key -> new TreeType(name, color, texture));
}
public static int poolSize() {
return pool.size();
}
}
public class Forest {
private record Tree(int x, int y, TreeType type) {}
private final List<Tree> trees = new ArrayList<>();
public void plant(int x, int y, String name, String color, String texture) {
trees.add(new Tree(x, y, TreeTypeFactory.get(name, color, texture)));
}
public void draw() {
for (Tree tree : trees) {
tree.type().draw(tree.x(), tree.y());
}
}
public static void main(String[] args) {
Forest forest = new Forest();
for (int i = 0; i < 1_000_000; i++) {
forest.plant(i % 800, i % 600, "Oak", "green", "oak.png");
}
System.out.println(TreeTypeFactory.poolSize()); // 1
}
}
public class TreeType
{
private readonly string _name;
private readonly string _color;
private readonly string _texture; // stands in for megabytes of pixels
internal TreeType(string name, string color, string texture) =>
(_name, _color, _texture) = (name, color, texture);
// x and y are extrinsic: supplied per call, never stored.
public void Draw(int x, int y) =>
Console.WriteLine($"Drawing {_color} {_name} at ({x}, {y})");
}
public static class TreeTypeFactory
{
private static readonly Dictionary<string, TreeType> Pool = new();
public static TreeType Get(string name, string color, string texture)
{
var key = $"{name}|{color}|{texture}";
if (!Pool.TryGetValue(key, out var type))
Pool[key] = type = new TreeType(name, color, texture);
return type;
}
public static int PoolSize => Pool.Count;
}
public class Forest
{
private readonly record struct Tree(int X, int Y, TreeType Type);
private readonly List<Tree> _trees = new();
public void Plant(int x, int y, string name, string color, string texture) =>
_trees.Add(new Tree(x, y, TreeTypeFactory.Get(name, color, texture)));
public void Draw()
{
foreach (var tree in _trees) tree.Type.Draw(tree.X, tree.Y);
}
}
var forest = new Forest();
for (var i = 0; i < 1_000_000; i++)
forest.Plant(i % 800, i % 600, "Oak", "green", "oak.png");
Console.WriteLine(TreeTypeFactory.PoolSize); // 1
#include <iostream>
#include <memory>
#include <string>
#include <unordered_map>
#include <vector>
class TreeType {
public:
TreeType(std::string name, std::string color, std::string texture)
: name_(std::move(name)),
color_(std::move(color)),
texture_(std::move(texture)) {}
// x and y are extrinsic: supplied per call, never stored.
void draw(int x, int y) const {
std::cout << "Drawing " << color_ << " " << name_ << " at (" << x << ", "
<< y << ")\n";
}
private:
std::string name_;
std::string color_;
std::string texture_; // stands in for a few megabytes of pixels
};
class TreeTypeFactory {
public:
static const TreeType& get(const std::string& name, const std::string& color,
const std::string& texture) {
const std::string key = name + "|" + color + "|" + texture;
auto [it, inserted] = pool().try_emplace(key, nullptr);
if (inserted) {
it->second = std::make_unique<TreeType>(name, color, texture);
}
return *it->second;
}
static std::size_t poolSize() { return pool().size(); }
private:
static std::unordered_map<std::string, std::unique_ptr<TreeType>>& pool() {
static std::unordered_map<std::string, std::unique_ptr<TreeType>> instance;
return instance;
}
};
struct Tree {
int x;
int y;
const TreeType* type; // shared, non-owning
};
int main() {
std::vector<Tree> forest;
for (int i = 0; i < 1'000'000; ++i) {
forest.push_back(
{i % 800, i % 600, &TreeTypeFactory::get("Oak", "green", "oak.png")});
}
std::cout << TreeTypeFactory::poolSize() << '\n'; // 1
}
from dataclasses import dataclass
from functools import lru_cache
@dataclass(frozen=True)
class TreeType:
name: str
color: str
texture: str # stands in for a few megabytes of pixels
# x and y are extrinsic: supplied per call, never stored.
def draw(self, x: int, y: int) -> None:
print(f"Drawing {self.color} {self.name} at ({x}, {y})")
# lru_cache is the flyweight factory: same arguments, same object back.
@lru_cache(maxsize=None)
def tree_type(name: str, color: str, texture: str) -> TreeType:
return TreeType(name, color, texture)
@dataclass(frozen=True)
class Tree:
x: int
y: int
type: TreeType
forest = [
Tree(i % 800, i % 600, tree_type("Oak", "green", "oak.png"))
for i in range(1_000_000)
]
print(tree_type.cache_info().currsize) # 1
use std::collections::HashMap;
use std::rc::Rc;
struct TreeType {
name: String,
color: String,
texture: String, // stands in for a few megabytes of pixels
}
impl TreeType {
// x and y are extrinsic: supplied per call, never stored.
fn draw(&self, x: i32, y: i32) {
println!("Drawing {} {} at ({x}, {y})", self.color, self.name);
}
}
#[derive(Default)]
struct TreeTypeFactory {
pool: HashMap<String, Rc<TreeType>>,
}
impl TreeTypeFactory {
// Rc is the sharing mechanism: every tree holds a cheap handle.
fn get(&mut self, name: &str, color: &str, texture: &str) -> Rc<TreeType> {
let key = format!("{name}|{color}|{texture}");
self.pool
.entry(key)
.or_insert_with(|| {
Rc::new(TreeType {
name: name.to_string(),
color: color.to_string(),
texture: texture.to_string(),
})
})
.clone()
}
}
struct Tree {
x: i32,
y: i32,
type_: Rc<TreeType>,
}
fn main() {
let mut factory = TreeTypeFactory::default();
let forest: Vec<Tree> = (0..1_000_000)
.map(|i| Tree {
x: i % 800,
y: i % 600,
type_: factory.get("Oak", "green", "oak.png"),
})
.collect();
forest[0].type_.draw(forest[0].x, forest[0].y);
println!("{}", factory.pool.len()); // 1
}
class TreeType {
constructor(
private readonly name: string,
private readonly color: string,
private readonly texture: string, // stands in for megabytes of pixels
) {}
// x and y are extrinsic: supplied per call, never stored.
draw(x: number, y: number): void {
console.log(`Drawing ${this.color} ${this.name} at (${x}, ${y})`);
}
}
class TreeTypeFactory {
private static readonly pool = new Map<string, TreeType>();
static get(name: string, color: string, texture: string): TreeType {
const key = `${name}|${color}|${texture}`;
let type = this.pool.get(key);
if (!type) {
type = new TreeType(name, color, texture);
this.pool.set(key, type);
}
return type;
}
static get poolSize(): number {
return this.pool.size;
}
}
interface Tree {
x: number;
y: number;
type: TreeType;
}
const forest: Tree[] = [];
for (let i = 0; i < 1_000_000; i++) {
forest.push({
x: i % 800,
y: i % 600,
type: TreeTypeFactory.get("Oak", "green", "oak.png"),
});
}
console.log(TreeTypeFactory.poolSize); // 1
A million trees, one TreeType instance.
Watch out¶
- Flyweights must be immutable; a shared object that one client mutates corrupts every other client.
- The factory pool is global state, in a multi-threaded program use a
ConcurrentHashMap. - This is a memory optimization. Do not apply it before you have measured a memory problem.
Check Your Understanding¶
#
What does the Flyweight Pattern optimise?