Structural Design Pattern: Flyweight

Photo by Sylvia Zhou on Unsplash

This article is the series exploring design patterns using the Java programming language. The goal of this series is to help readers develop a solid understanding of design patterns while also sharing real-world examples from actual codebases that make use of these patterns.

In this article, we’ll be discussing the Flyweight pattern.

Symbols for better navigation:

🤔 Hypothetical Scenario/Imagination

⚠️ Warning

👉 Point to note

📝 Common Understanding

Flyweight Pattern

The Flyweight pattern is a structural design pattern that lets you fit more objects into the available amount of memory by sharing common parts of state between multiple objects instead of keeping all of the data in each object.

While the Singleton pattern was created to ensure a single instance of a class exists, the Flyweight pattern was created to handle millions of instances without crashing your memory.

🤔 Imagine you are building a massive multiplayer war game. You have a Particle class to represent bullets, shrapnel, and missile trails. Each particle has coordinates (x, y), a vector (speed), a color, and a specific sprite image (which might be a few kilobytes).

📉 If a battle creates a million particles, and each particle object holds its own copy of the sprite image and color data, your memory will overflow, and the game will crash.

We need a way to create millions of these objects, but share the heavy data (like the image) so it isn’t duplicated many times in memory.

Enter… The Flyweight.

To understand Flyweight, you must separate your object’s data into two types:

  • Extrinsic State (Unique): Data that changes for every object (e.g., the x, y coordinates, the current speed). This is context-dependent.
  • Intrinsic State (Shared): Data that remains constant and is shared among many objects (e.g., the color of the bullet, the sprite image). This is context-independent.

Recipe to cook the Flyweight Pattern for objects

Any class can be converted to use Flyweight by following these steps:

  1. Identify the Intrinsic state (heavy, shared data) and remove it from the main class.
  2. Create a new class (the Flyweight) to store this intrinsic state. Make it immutable.
  3. Keep the Extrinsic state in the main class (or pass it to methods when needed).
  4. Create a Factory class to manage a pool of Flyweights.

Case Study

In our example, we will build an Inventory Management System (IMS).

We start with an Item class, which holds intrinsic properties like name and price, data that remains constant across all transactions.

Java
class Item {
  String itemName;
  Double price;
  
  public Item(String itemName, Double price) {
    this.itemName = itemName;
    this.price = price;
  }
}

Next, we introduce an Order class to track the quantity of items purchased.

Since the quantity varies from order to order, this represents the extrinsic state.

Java
class Order {
  Item item;
  int quantity;
  
  public Order(Item item, int quantity) {
    this.item = item;
    this.quantity = quantity;
  }
}

Finally, our main class, InventoryManagementSystem, acts as the factory; it maintains a pool of unique Item objects and facilitates the creation of new orders.

Java
class InventoryManagementSystem {
    private Map<String, Item> itemsMap;
    private List<Order> orderList;

    public InventoryManagementSystem() {
        this.itemsMap = new HashMap<>();
        this.orderList = new ArrayList<>();
    }

    public void placeOrder(String itemName, int quantity, double price) {
        if (!itemsMap.containsKey(itemName)) {
            itemsMap.put(itemName, new Item(itemName, price));
        }
        orderList.add(new Order(itemsMap.get(itemName), quantity));
    }
}
 

Let’s tie up everything see how things play out.

Java
public class Main {
    public static void main(String[] args) {
        InventoryManagementSystem ims = new InventoryManagementSystem();

        ims.placeOrder("Soap", 2, 12.0);
        ims.placeOrder("Shampoo", 1, 110.0);
        ims.placeOrder("Bathrobe", 1, 1000.0);
        ims.placeOrder("Shampoo", 1, 110.0);
        ims.placeOrder("Soap", 1, 12.0);

        ims.printOrders();
    }
}

As you can see, creating multiple orders now does not require us to instantiate different items in the runtime. Rather, multiple orders share the common state values of itemName and price.

Real-World Use Case of Proxy

Here is an example from the real-world code where they have used a Flyweight.

Java String Constant Pool: The implementation of Flyweight in Java is the String class.

When you do this:

Java
String s1 = "Hello";
String s2 = "Hello";

Java doesn’t create two separate objects in the memory heap for the text Hello. Inside the JVM, there is a special memory region called the String Constant Pool.

  1. When s1 is created, the JVM checks the pool. It’s empty, so it creates “Hello” and returns the reference.
  2. When s2 is created, the JVM checks the pool. It finds “Hello” already exists!
  3. It simply returns the reference to the existing object.

Both s1 and s2 point to the exact same memory address. This saves a massive amount of memory in large applications that process text.

Up next in the Series

Bridge Design Pattern

Subscribe to my newsletter today!

Share it on