Flyweight Pattern in C# – Optimize Your Memory Usage!

dazevedo

Daniel Azevedo

Posted on October 26, 2024

Flyweight Pattern in C# – Optimize Your Memory Usage!

Hi devs
When working with large datasets or objects that have many duplicate instances, optimizing memory usage becomes critical. This is where the Flyweight Pattern comes into play! In this post, we'll go through what the Flyweight Pattern is, why it's useful, and how to implement it in C# with a clear example.


What is the Flyweight Pattern?

The Flyweight Pattern is a structural design pattern that focuses on reducing memory consumption by sharing as much data as possible between similar objects. It’s particularly useful when:

  • You have a large number of similar objects.
  • The memory cost of each object is high.
  • Some object data can be shared while other parts must remain unique.

Flyweight helps by creating shared objects that can be reused across multiple contexts, rather than creating new instances each time.


When Should You Use the Flyweight Pattern?

Consider scenarios where:

  • You are dealing with a large set of objects with common data (e.g., graphical elements in games, characters in text processing, etc.).
  • You want to avoid the high memory cost of storing repetitive data in each instance.

A classic example is text rendering: each character can share its font style, color, and font size, while only position and specific details differ.


Example: Implementing Flyweight Pattern in C

Let's use a scenario where we manage a large number of employee records in an HR system. Each employee may share common data, like department or job role, but has unique attributes like name and ID. With the Flyweight Pattern, we can share the department data among employees to save memory.


Step 1: Define the Flyweight Class

The Flyweight class will hold the intrinsic (shared) state, like the employee’s role or department.

public class EmployeeType
{
    public string Department { get; }
    public string Role { get; }

    public EmployeeType(string department, string role)
    {
        Department = department;
        Role = role;
    }

    public void Display(string name, int id)
    {
        Console.WriteLine($"Employee: {name}, ID: {id}, Department: {Department}, Role: {Role}");
    }
}
Enter fullscreen mode Exit fullscreen mode

In this example, Department and Role are intrinsic data (the shared part). We'll keep this information in the EmployeeType class and use it across multiple Employee instances.


Step 2: Create the Flyweight Factory

The Flyweight Factory is responsible for managing and reusing flyweight instances. If a flyweight with the same intrinsic data already exists, it returns that instance; otherwise, it creates a new one.

public class EmployeeTypeFactory
{
    private readonly Dictionary<string, EmployeeType> _employeeTypes = new();

    public EmployeeType GetEmployeeType(string department, string role)
    {
        string key = department + "_" + role;

        if (!_employeeTypes.ContainsKey(key))
        {
            _employeeTypes[key] = new EmployeeType(department, role);
            Console.WriteLine($"Creating new EmployeeType: {department}, {role}");
        }

        return _employeeTypes[key];
    }
}
Enter fullscreen mode Exit fullscreen mode

The EmployeeTypeFactory class manages the storage of EmployeeType instances and reuses them when possible.


Step 3: Use the Flyweight Objects

Now, let’s create individual Employee objects that use EmployeeType objects as flyweights for shared data.

public class Employee
{
    private readonly string _name;
    private readonly int _id;
    private readonly EmployeeType _type;

    public Employee(string name, int id, EmployeeType type)
    {
        _name = name;
        _id = id;
        _type = type;
    }

    public void Display()
    {
        _type.Display(_name, _id);
    }
}
Enter fullscreen mode Exit fullscreen mode

Each Employee object has a unique name and id but shares an EmployeeType for common data. This allows us to significantly reduce memory usage by reusing EmployeeType instances.


Step 4: Putting It All Together

Finally, let's see how to use these classes together. Here’s how we can create and display employees while reusing EmployeeType instances.

public class Program
{
    public static void Main()
    {
        var factory = new EmployeeTypeFactory();

        var developerType = factory.GetEmployeeType("Engineering", "Developer");
        var designerType = factory.GetEmployeeType("Design", "UI Designer");

        var employee1 = new Employee("Alice", 1, developerType);
        var employee2 = new Employee("Bob", 2, developerType); // Reuses developerType
        var employee3 = new Employee("Charlie", 3, designerType);

        employee1.Display();
        employee2.Display();
        employee3.Display();
    }
}
Enter fullscreen mode Exit fullscreen mode

When we run this, the factory reuses the EmployeeType instances, reducing the memory overhead by not duplicating the Department and Role data.

Expected Output:

Creating new EmployeeType: Engineering, Developer
Creating new EmployeeType: Design, UI Designer
Employee: Alice, ID: 1, Department: Engineering, Role: Developer
Employee: Bob, ID: 2, Department: Engineering, Role: Developer
Employee: Charlie, ID: 3, Department: Design, Role: UI Designer
Enter fullscreen mode Exit fullscreen mode

Notice that only two EmployeeType instances were created, even though we have three Employee objects. By reusing these instances, we save memory, especially useful when we scale up to thousands of employees with similar roles.


Benefits of Using the Flyweight Pattern

  • Memory Efficiency: By sharing common data, we reduce the memory footprint of similar objects.
  • Improved Performance: Reducing memory usage can lead to performance gains, particularly in memory-bound applications.
  • Easier to Manage: Helps to separate intrinsic and extrinsic states, making the code more organized.

Drawbacks of the Flyweight Pattern

  • Complexity: Adding the Flyweight Pattern can add a bit of complexity, as you need to distinguish between intrinsic and extrinsic state.
  • Limited Use Cases: It’s only useful when you have large sets of similar objects with substantial shared data.

Conclusion

The Flyweight Pattern is a powerful tool for managing memory when dealing with large numbers of objects with shared data. In scenarios where memory consumption is critical, like game development, large databases, or UI rendering, the Flyweight Pattern can be a game-changer.

Give it a try in your next C# project and see the impact on performance and memory efficiency!

Keep coding

💖 💪 🙅 🚩
dazevedo
Daniel Azevedo

Posted on October 26, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related