Design Pattern (16) Flyweight Pattern: Memory Optimization and Performance Enhancement Guide

You can download the Design Pattern series code from this design_pattern repo.

Requirements

Suppose we are developing a forest scene rendering system. This system needs to display hundreds or even thousands of trees on screen, providing rich forest environments for games or visualization applications.

When designing this system, we discovered that each tree contains two different types of data:

  1. Intrinsic State: Common data that doesn’t change with the environment, such as tree species, color, texture, etc. This data is consistent among all trees of the same type.
  2. Extrinsic State: Unique data that varies by environmental position, such as each tree’s coordinates (x, y) on screen.

Core Problem: If we create complete objects for each tree, it will lead to excessive memory consumption. Imagine when we need to render 10,000 oak trees, each tree object stores the same species, color, and texture information.

Therefore, we need a way to share intrinsic state to optimize memory usage.

Object-Oriented Analysis (OOA)

After understanding the requirements, let’s quickly implement object-oriented analysis!

Identifying Forces

When analyzing design requirements in depth, we identified three main design challenges:

1. Massive Duplicate Data Problem Each tree contains the same species, color, and texture data. This redundant storage causes unnecessary memory waste, especially when the forest has thousands of trees of the same type.

2. Performance Bottleneck Problem For large scenes that need to render thousands of trees, too many object instances can lead to memory insufficiency or severe performance bottlenecks. The system may become slow or even crash due to memory pressure.

3. Balance Between Sharing and Independence Problem We need to share common data while ensuring each tree can still maintain its independent position information. Finding this balance point is key to the design.

Solution-Oriented: Facing these challenges, the Flyweight Pattern provides an elegant solution that allows us to effectively share objects’ intrinsic state.

Applying Flyweight Pattern (Solution) to Achieve New Context (Resulting Context)

Now we have completed object-oriented analysis and clearly identified various constraints and challenges in the design. Next, let’s apply the Flyweight Pattern to solve this memory optimization problem.

First, let’s understand the general structure of the Flyweight Pattern:

The Flyweight Pattern contains four core roles, each with specific responsibilities:

  • Flyweight (Flyweight Interface): Defines the common interface that all flyweight objects must implement, specifying operation methods for shared objects.
  • ConcreteFlyweight (Concrete Flyweight Class): Concrete class implementing the flyweight interface, responsible for storing and managing sharable intrinsic state.
  • FlyweightFactory (Flyweight Factory): Factory class responsible for creating and managing flyweight objects, ensuring objects with same characteristics are created only once and providing fast access mechanisms.
  • Client (Client): Code that uses flyweight objects, also responsible for managing and passing non-sharable extrinsic state.

Applying to Our Forest Rendering System

Now let’s concretely apply this pattern concept to our tree rendering requirements:

Object-Oriented Programming (OOP)

Step 1: Define Flyweight Objects and Context Objects

We separate tree data into two classes: TreeType (flyweight object, stores shared intrinsic state) and Tree (context object, stores unique extrinsic state).

class Tree(
    private val x: Int,
    private val y: Int,
    private val type: TreeType
) {
    fun draw() {
        type.draw(x, y)
    }
}

class TreeType(
    val name: String,
    val color: String,
    val texture: String
) {
    fun draw(x: Int, y: Int) {
        println("Drawing tree: $name, color: $color, texture: $texture at ($x, $y)")
    }
}

Step 2: Build Flyweight Factory

TreeFactory is responsible for managing and reusing TreeType objects, ensuring tree types with same characteristics are created only once.

object TreeFactory {
    private val treeTypes = mutableMapOf<String, TreeType>()

    fun getTreeType(name: String, color: String, texture: String): TreeType {
        return treeTypes.computeIfAbsent(name) {
            println("Creating new TreeType: $name")
            TreeType(name, color, texture)
        }
    }
}

Step 3: Implement Client Management Class

Forest class serves as the client, responsible for managing all tree objects and coordinating the combination of intrinsic and extrinsic states.

class Forest {
    private val trees = mutableListOf<Tree>()

    fun plantTree(x: Int, y: Int, name: String, color: String, texture: String) {
        val treeType = TreeFactory.getTreeType(name, color, texture)
        val tree = Tree(x, y, treeType)
        trees.add(tree)
    }

    fun draw() {
        for (tree in trees) {
            tree.draw()
        }
    }
}

Step 4: Testing and Validation

Test our Flyweight Pattern implementation through the main function, observing the memory optimization effects.

fun main() {
    val forest = Forest()

    // Planting trees in the forest
    forest.plantTree(10, 20, "Oak", "Green", "Rough")
    forest.plantTree(15, 25, "Pine", "Dark Green", "Smooth")
    forest.plantTree(10, 20, "Oak", "Green", "Rough") // Reuses the same TreeType as the first Oak

    // Draw all trees
    forest.draw()
}

Execution Results Analysis

From the output results, we can clearly see the core benefits of the Flyweight Pattern:

Creating new TreeType: Oak
Creating new TreeType: Pine
Drawing tree: Oak, color: Green, texture: Rough at (10, 20)
Drawing tree: Pine, color: Dark Green, texture: Smooth at (15, 25)
Drawing tree: Oak, color: Green, texture: Rough at (10, 20)

Conclusion

Core Benefits of Flyweight Pattern

Through implementing the Flyweight Pattern, we successfully achieved the following key improvements:

Memory Optimization: Through sharing technology, dramatically reduced the system’s memory usage. In our example, even when planting multiple trees of the same type, TreeType objects are created only once.

Performance Enhancement: Reduced object creation overhead, improving overall system performance, especially noticeable when handling large numbers of similar objects.

Good Scalability: Adding different tree types became simple, only requiring registration of new tree species in the factory.

Applicable Scenarios and Considerations

The Flyweight Pattern is particularly suitable for the following application scenarios:

  • Text Editors: Sharing character objects (text with same characters, fonts, sizes)
  • Game Development: Large numbers of similar game objects in scenes (bullets, particle effects, NPCs)
  • Graphics Rendering: Repeated graphic elements or materials

Key Design Considerations: When using the Flyweight Pattern, the most important thing is correctly distinguishing intrinsic state from extrinsic state. Intrinsic state must be safely sharable immutable data, while extrinsic state is unique information for each object instance. Only by clearly understanding this distinction can we ensure the correctness and flexibility of system design.

Series Navigation

Structural Design Pattern Series

  • Adapter Pattern - Making incompatible interfaces work together
  • Bridge Pattern - Separating abstraction from implementation, supporting independent evolution
  • Composite Pattern - Uniformly handling individual objects and object combinations
  • Decorator Pattern - Dynamically adding object functionality without modifying structure
  • Facade Pattern - Providing unified interface to simplify complex subsystems
  • Proxy Pattern - Controlling resource access through smart proxy objects

Behavioral Design Pattern Series

Creational Design Pattern Basics

Through the Flyweight Pattern, we learned how to effectively manage large numbers of similar objects through object sharing technology. In the next article on the Proxy Pattern, we will explore another structural design technique for controlling object access.




    Enjoy Reading This Article?

    Here are some more articles you might like to read next:

  • Claude Code 使用技巧與最佳實踐 - Tips and Best Practices
  • 🤖 AI Agent Series (Part 1): Understanding the Core Interaction Logic of LLM, RAG, and MCP
  • 💡 Managing Multiple GitHub Accounts on One Computer: The Simplest SSH Configuration Method
  • 🚀 How to Use Excalidraw AI to Quickly Generate Professional Diagrams and Boost Work Efficiency!
  • Complete macOS Development Environment Setup Guide: Mobile Development Toolchain Configuration Tutorial