Flyweight
Use sharing to support large numbers of fine-grained objects efficiently.
Participants
| Role | In this example | Responsibility |
|---|---|---|
| Flyweight | TileStyle | Holds the intrinsic state that's the same for every tile of a kind. Frozen, because it's shared. |
| Flyweight Factory | TileStyles | Creates each style once and hands out the same object after that. |
| Context | Tile | Holds extrinsic state (its position) and a reference to its shared style. |
| Client | Terrain | Builds a tile for every character of the map, asking the factory for styles. |
Ruby implementation
# The Flyweight: how one kind of terrain looks. Shared by every tile of that
# kind, so it's frozen: a shared object must never change under its users.
class TileStyle
attr_reader :char, :colour
def initialize(char, colour)
@char = char
@colour = colour
freeze
end
end
# The Flyweight Factory: hands out one shared style per kind of terrain.
class TileStyles
LOOKS = { "." => [".", :green], "=" => ["=", :tan], "~" => ["~", :blue] }.freeze
def initialize
@styles = {}
end
def fetch(symbol)
@styles[symbol] ||= TileStyle.new(*LOOKS.fetch(symbol))
end
def count = @styles.size
end
# The Context: each tile keeps only its own position, plus a shared style.
Tile = Data.define(:x, :y, :style)
class Terrain
attr_reader :tiles
def initialize(layout, styles)
@tiles = layout.lines.each_with_index.flat_map do |row, y|
row.chomp.chars.each_with_index.map { |symbol, x| Tile.new(x:, y:, style: styles.fetch(symbol)) }
end
end
endIn use
styles = TileStyles.new
terrain = Terrain.new(<<~MAP, styles)
........
========
......~~
MAP
terrain.tiles.size # => 24
styles.count # => 3 (three styles in memory, not twenty-four)
On a real map there are thousands of tiles, and almost all of them look like one of a handful of styles.
Notes
Ruby idioms
hash[key] ||= ...is the whole flyweight factory in one line.- Ruby already uses flyweights. Symbols, small integers,
nil,trueand frozen string literals are shared objects.:grass.equal?(:grass)is always true. freezethe flyweight. Sharing is only safe if nobody can change the shared state.
Reach for it when
- You have very many objects whose memory is mostly the same repeated data: map tiles, characters in a document, particles.
- You’ve measured a memory or allocation problem. Flyweight trades simplicity for memory.
Watch for
- The cache never shrinks. For unbounded keys, consider a size limit or
ObjectSpace::WeakMap.
Share what doesn’t change. Store what does on each object.