Flyweight

Use sharing to support large numbers of fine-grained objects efficiently.

Participants

RoleIn this exampleResponsibility
FlyweightTileStyleHolds the intrinsic state that's the same for every tile of a kind. Frozen, because it's shared.
Flyweight FactoryTileStylesCreates each style once and hands out the same object after that.
ContextTileHolds extrinsic state (its position) and a reference to its shared style.
ClientTerrainBuilds a tile for every character of the map, asking the factory for styles.

Ruby implementation

flyweight.rb
# 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
end

In 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, true and frozen string literals are shared objects. :grass.equal?(:grass) is always true.
  • freeze the 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.