Menu Close

Object Oriented Design Pattern: Flyweight

flyweight winning

Explanation

The Flyweight design pattern is a structural pattern that is used to optimize and conserve memory consumption. For instance, when you use a lot of objects, the flyweight pattern allows you to share parts of these objects through a flyweight class. This class has two states, the intrinsic and the extrinsic state. The intrinsic state is the shared part, the same for all objects which use it. The extrinsic state depends on the context of which objects use the flyweight and make it non-shareable. The example will hopefully make that more clear.

The flyweight design pattern is useful when you need to create a large number of objects, but do not need to keep track of all information of all of the objects. For instance, when you want to draw a lot of circles on the screen, you can re-use the same circle object. You instantiate a circle object, fill in its parameters, draw the circle, set new parameters and draw the next circle. The first circle object cannot be referenced anymore, but is already drawn. So if this suffices for your use case, it will help you save memory.

In this example, the extrinsic data (unique data for each instance) are possibly the x and y coordinates and size of the circle. The intrinsic (shared) data might be the texture or the color of the circle, which will be re-used. In short, the minimizing of memory happens by reducing the number of objects that will be created and by sharing common data.

Use cases

  • You need a large number of objects and need to go easy on memory use
  • You do not need to keep unique references to each and every object

Pitfalls

  • The intrinsic data is not sufficient to make a difference, thus making the pattern obsolete

General part of the Example code

I want to continue with the simple use case of drawing circles on a canvas. First I will list the code for the xaml files and after that I will explain the drawing of circles with and without making use of the flyweight pattern.

Project files

XAML

For the demo application I use a WPF (Windows Presentation Foundation) project. For reference and completeness, these are the App.xaml and MainWindow.xaml files.

App.xaml

<Application x:Class="Flyweight.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:Flyweight"
             StartupUri="MainWindow.xaml">
    <Application.Resources>
         
    </Application.Resources>
</Application>
XML

MainWindow.xaml

This will create a new window that contains a stackpanel with a canvas and textbox inside.

<Window x:Class="Flyweight.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:Flyweight"
        mc:Ignorable="d"
        Title="MainWindow" Height="600" Width="500">
    <StackPanel Orientation="Vertical">
        <Canvas x:Name="myCanvas" Width="500" Height="500"></Canvas>
        <TextBox Focusable="False" x:Name="MemoryDisplay" TextWrapping="Wrap" Text="TextBox" FontSize="20"/>
    </StackPanel>
</Window>
XML

C#

App.xaml.cs

If you are not familiar with WPF, the App.xaml.cs file is coupled to the App.xaml file. This is the entrance point of the application. I added two static functions for convenience. The MainWindow.xaml is started from the App.xaml file. The code continues at MainWindows.xaml.cs.

using System.Diagnostics;
using System.Windows;

namespace Flyweight
{
    /// <summary>
    /// Interaction logic for App.xaml
    /// </summary>
    public partial class App : Application
    {

        public static int RandomInt(int maxValue)
        {
            Random random = new Random();
            return random.Next(maxValue);
        }

        public static string PrintMemory()
        {
            Process currentProcess = Process.GetCurrentProcess();
            return $"Memory Usage: {currentProcess.WorkingSet64 / 1024 / 1024} MB";
        }
    }
}
C#

Circle.cs

Before we go further with the MainWindow.cs file, I will first shortly list the Circle and Flyweight classes. The Circle class is made out of intrinsic and extrinsic data as I mentioned in the Explanation. The intrinsic data cannot be shared, thus I simulated that it will contain approximately 10MB’s to show the saving of memory later with the use of the flyweight design pattern.

using System.Windows;
using System.Windows.Media;

namespace Flyweight
{
    public class Circle
    {
        // Intrinsic data
        private Color color;
        private byte[] largeData; // simulated large intrinsic data

        // Extrinsic data
        public double X { get; set; }
        public double Y { get; set; }
        public double Radius { get; set; }

        public Circle(Color color)
        {
            this.color = color;
            this.largeData = GenerateLargeData((1024 * 1024 * 10)); // generate 10MB
        }

        private byte[] GenerateLargeData(int size)
        {
            byte[] data = new byte[size];
            Random random = new Random();
            random.NextBytes(data);
            return data;
        }

        public void Draw(DrawingContext dc, Circle shape)
        {
            dc.DrawEllipse(new SolidColorBrush(this.color), null, new Point(shape.X, shape.Y), shape.Radius, shape.Radius);
        }
    }
}
C#

Example without Flyweight design pattern

MainWindow.xaml.cs

The code does the following:

  • Constructor gets called and the code will continue when the Window has been loaded.
  • The DrawingVisual object, which is a class scoped variable, will be used to draw circles on a DrawingContext.
  • The RenderTargetBitmap renders the DrawingVisual.
  • An Image is created with the Image Source set to the RenderTargetBitmap.
  • The Image is added to the Canvas defined in MainWindow.xaml.
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Diagnostics;

namespace Flyweight
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        int drawNumber = 100;
        DrawingVisual drawingVisual = new DrawingVisual();

        // constructor
        public MainWindow()
        {
            InitializeComponent();

            this.Loaded += delegate (object sender, RoutedEventArgs args)
            {
                DrawCircles();
                CreateBitmap();
                MemoryDisplay.Text = App.PrintMemory();
            };
        }

        void CreateBitmap()
        {
            RenderTargetBitmap rtb = new RenderTargetBitmap((int)myCanvas.ActualWidth, (int)myCanvas.ActualHeight, 96, 96, PixelFormats.Pbgra32);
            rtb.Render(drawingVisual);
            Image image = new Image();
            image.Source = rtb;
            myCanvas.Children.Add(image);
        }

        Color RandomColor()
        {
            Color[] colorArray = { Colors.RosyBrown, Colors.DarkGreen, Colors.Lavender, Colors.Maroon };
            Random random = new Random();
            return colorArray[random.Next(0, colorArray.Length)];
        }

        void DrawCircles()
        {
            try
            {
                using (DrawingContext dc = drawingVisual.RenderOpen())
                {
                    for (int i = 0; i < drawNumber; ++i)
                    {
                        Circle circle = new Circle(RandomColor());
                        circle.X = App.RandomInt(500);
                        circle.Y = App.RandomInt(450);
                        circle.Radius = App.RandomInt(50);
                        circle.Draw(dc, circle);
                    }
                }
            }
            catch (Exception ex)
            {
                Debug.WriteLine(ex.ToString());
            }
        }
    }
}
C#

This gives the following result, which as you can see, uses 303MBs of memory.

303MBs used without the Flyweight design pattern

Example with Flyweight design pattern

Flyweight.cs

For convenience of the example, I called the file Flyweight.cs. In the background, there is just a ShapeFactory (Factory Design Pattern). What this does is keeping a Dictionary that is indexed by Color. Only when a new color is detected when calling the GetCircle method, a new Circle with that color will be created. In this example, the simulated largeData object belongs together with a color. So every time a new Circle object will be created, so will 10MB of data.

using System.Windows.Media;

namespace Flyweight
{
    public interface IShape
    {
        double X { get; set; }
        double Y { get; set; }  
        double Radius { get; set; }
        void Draw(DrawingContext dc, IShape shape);
    }

    public class ShapeFactory
    {
        public static Dictionary<Color, IShape> circleMap = new Dictionary<Color, IShape>();

        public static IShape GetCircle(Color color)
        {
            Circle circle = null;
            if (circleMap.ContainsKey(color))
            {
                circle = (Circle)circleMap[color];
            }
            else
            {
                circle = new Circle(color);
                circleMap[color] = circle;
            }
            return circle;
        }
    }
}
C#

Alter Circle.cs

To make use of the Flyweight pattern, we need to make use of the ShapeFactory. To do so we can alter Circle to the following:

using System.Windows;
using System.Windows.Media;

namespace Flyweight
{
    public class Circle : IShape
    {
        // Intrinsic data
        private Color color;
        private byte[] largeData; // simulated large intrinsic data

        // Extrinsic data
        public double X { get; set; }
        public double Y { get; set; }
        public double Radius { get; set; }

        public Circle(Color color)
        {
            this.color = color;
            this.largeData = GenerateLargeData((1024 * 1024 * 10)); // generate 10MB
        }

        private byte[] GenerateLargeData(int size)
        {
            byte[] data = new byte[size];
            Random random = new Random();
            random.NextBytes(data);
            return data;
        }

        public void Draw(DrawingContext dc, IShape shape)
        {
            dc.DrawEllipse(new SolidColorBrush(this.color), null, new Point(shape.X, shape.Y), shape.Radius, shape.Radius);
        }
    }
}
C#

Alter MainWindow.cs

using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Diagnostics;

namespace Flyweight
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        int drawNumber = 100;
        DrawingVisual drawingVisual = new DrawingVisual();

        public MainWindow()
        {
            InitializeComponent();

            this.Loaded += delegate (object sender, RoutedEventArgs args)
            {
                DrawCirclesFlyweight(); 
                //DrawCircles();
                CreateBitmap();
                MemoryDisplay.Text = App.PrintMemory();
            };
        }

        void CreateBitmap()
        {
            RenderTargetBitmap rtb = new RenderTargetBitmap((int)myCanvas.ActualWidth, (int)myCanvas.ActualHeight, 96, 96, PixelFormats.Pbgra32);
            rtb.Render(drawingVisual);
            Image image = new Image();
            image.Source = rtb;
            myCanvas.Children.Add(image);
        }

        Color RandomColor()
        {
            Color[] colorArray = { Colors.RosyBrown, Colors.DarkGreen, Colors.Lavender, Colors.Maroon };
            Random random = new Random();
            return colorArray[random.Next(0, colorArray.Length)];
        }

        void DrawCirclesFlyweight()
        {
            try
            {
                using (DrawingContext dc = drawingVisual.RenderOpen())
                {
                    for (int i = 0; i < drawNumber; ++i)
                    {
                        Circle circle = (Circle)ShapeFactory.GetCircle(RandomColor());
                        circle.X = App.RandomInt(500);
                        circle.Y = App.RandomInt(450);
                        circle.Radius = App.RandomInt(50);
                        circle.Draw(dc, circle);
                    }
                }
            }
            catch (Exception ex)
            {
                Debug.WriteLine(ex.ToString());
            }
        }

        void DrawCircles()
        {
            try
            {
                using (DrawingContext dc = drawingVisual.RenderOpen())
                {
                    for (int i = 0; i < drawNumber; ++i)
                    {
                        Circle circle = new Circle(RandomColor());
                        circle.X = App.RandomInt(500);
                        circle.Y = App.RandomInt(450);
                        circle.Radius = App.RandomInt(50);
                        circle.Draw(dc, circle);
                    }
                }
            }
            catch (Exception ex)
            {
                Debug.WriteLine(ex.ToString());
            }
        }
    }
}
C#

About 120MBs of memory is saved with the Flyweight design pattern.

180MBs used with the Flyweight design pattern

Class diagram

Class diagram

In the example, ShapeFactory is our Flyweight class.

Conclusion

The flyweight design pattern is useful for conserving memory. If your application uses a great amount of objects where you do not keep track of each instance individually, you can consider implementing the pattern. The most useful implementation is when you have a great amount of intrinsic data and/or a lot of objects to create.

Common use cases are where you need to draw things on the screen, but can also be used for other use cases, such as to add the same meta-data object to multiple documents in a document management system. It can also be used in game development to render repetitive objects like environmental elements and in text editors to render characters.

References

Freeman, E., Bates, B., & Sierra, K. (2004). Head first design patterns.

Gamma, E., Helm, R., Johnson, R., & Vlissides, J. (1994). Design Patterns: Elements of Reusable Object-Oriented Software.

Wikipedia contributors. (2024, 10 september). Software design pattern. Wikipedia. https://en.wikipedia.org/wiki/Software_design_pattern

Related Posts