Flyweight Pattern in C#
Kostas Kalafatis
Posted on May 9, 2023
The Flyweight design pattern is a structural pattern that optimizes memory usage by allowing multiple objects to share a common state instead of maintaining redundant data for each object.
The main goal of the Flyweight pattern is to make an application use less memory by letting similar objects share as much information as possible. It does this by splitting an item into intrinsic and extrinsic states. The intrinsic state is the data that an object needs to work on its own, and the extrinsic state is the context-specific data that related entities can share. By sharing the extrinsic state and keeping the intrinsic state separate, Flyweight can reduce an application's total memory use without changing how it works.
You can find the example code for this post on GitHub
Conceptualizing the Problem
If you haven't played Minecraft yet, you have no idea what you're missing out on; the rest of us have plenty of experience with the game. Because of how well it was received, our company came up with the idea to create a new game that is somewhat like it and title it CraftMine. Since we had arrived at this point, our team came to the conclusion that the best way to create a large planet with gorgeous topography would be to base it on squares. Stone mountains, gorges hundreds of feet deep, rivers and valleys, tunnels, and mines combine to provide the player with a breathtaking adventure.
Once everything was ready, we pushed the final commit, built the game, and gave it to several people so they could try it out. Even though the game ran easily on the systems we were using, our friends couldn't play it for a long time. After just a few minutes of playing on their computers, the game would often crash without warning.
After analyzing the debug logs, we determined that the game froze due to insufficient RAM. It became apparent that the computers our friends were using were less powerful than our business computers, which is why the issue surfaced much earlier on their machines. Our computers possess greater processing power than our friends.
The actual issue was with our block system. A single object that could store a sizable amount of data served as the representation for each block, such as a mud block, a wood block, or a leaf block. When the blocks on a player's screen reached a certain threshold, newly produced blocks could no longer fit into the remaining RAM, and the application crashed.
If you look closely at the Block class, you'll discover that the StepSound
and Texture
fields use a lot more memory than the other fields. Worse, these two fields contain nearly identical data across all particles. All grass blocks, for example, have the same sound effect and texture.
Other aspects of a block's state are unique to each block, such as coordinates, if it is breakable, or its light level. After all, these fields' values fluctuate over time. This data depicts the constantly changing context in which the block exists, although the block's sound effects and texture remain constant.
The intrinsic state of an entity refers to its constant data. It is inherent in the object and is not modifiable by other objects. The rest of the object's state, which is frequently affected "from the outside" by other things, is referred to as the extrinsic state.
The Flyweight pattern suggests that we stop storing the extrinsic state inside the object. Instead, we should pass this state to specific methods that rely on it. Instead, we should pass this state to specific methods which rely on it. On the other hand, the intrinsic state stays within the object, letting us reuse it in different contexts. As a result, we'd need fewer of these objects since they only differ in the intrinsic state, which has much fewer variations than the extrinsic.
Storing the extrinsic state
Now that we have removed the extrinsic state from our blocks, we need to store them somewhere. Most of the time, the extrinsic state is moved to a container object, which aggregates objects before applying the pattern.
Back to our CraftMine project, we can use the Game
class to store all the blocks in the blocks
field. To incorporate the extrinsic state of the blocks, we first need to declare multiple arrays for storing coordinates, light levels, and other properties of each unique block. Moreover, we need another array to store a reference to a specific flyweight that represents a block. These arrays then must be synchronized so that we may access all of a block's data using the same index. This is a pretty inelegant solution.
A more elegant solution would be to define a new context class that will store the extrinsic state as well as a reference to the flyweight object. This solution would require having a single array in the container class.
This might require the same number of contextual instances as we did before; however, these instances are much less memory intensive. The heaviest fields are now concentrated on a few flyweight objects. Instead of maintaining a million copies of the same data, a million little contextual objects can now utilize a single large flyweight object.
Flyweight objects and Immutability
We must ensure that the flyweight object's state cannot be changed because it can be used in various contexts. A flyweight's state should only ever be initialized once via constructor parameters. Other objects shouldn't be able to access any setters or public fields.
Flyweight Factories
We can create a factory method that manages a pool of existing flyweight objects for more convenient access. The method will accept the intrinsic state of the desired flyweight from a client, then look for an existing flyweight object that matches this state, and finally return it if it was found. If not the flyweight will be added into the pool.
Structuring the Flyweight Pattern
In the basic implementation, the Flyweight pattern has 4 main participants:
- Flyweight: The part of the original object's state that can be shared by multiple objects is contained in the Flyweight class. The same flyweight object can be applied in a variety of situations. The term "intrinsic" refers to the state kept inside a flyweight. Extrinsic refers to the state that was passed to the flyweight's methods.
- Concrete Flyweight: The extrinsic state, which is exclusive to all original objects, is contained in the ConcreteFlyweight class. One of the flyweight objects and a concrete flyweight together represent the complete state of the original object.
- Flyweight Factory: The Flyweight Factory is responsible for managing a collection of current flyweights. Clients don't directly produce flyweights at the factory. Instead, they make a call to the factory and send it pieces of the desired flyweight's intrinsic state. The factory searches its database of previously produced flyweights and either returns one that already exists and matches the search criteria or if none are found, creates a new one.
- Client: The extrinsic state of flyweights is computed or stored by the Client. A flyweight is a template object that can be configured at runtime from the client's perspective by passing some contextual data into its method parameters.
To demonstrate how the flyweight pattern works we are going to create a drawing application. For our example, let's say we need to create a drawing containing different shapes, lines, ovals, triangles and squares. So first we need to create an IShape
interface:
namespace Flyweight.Shapes;
public interface IShape
{
void Draw(Graphics g, int x, int y, int width, int height, Color color);
}
Now let's define our shapes. The Line
class has no intrinsic properties, but Oval
, Square
and Triangle
all have an intrinsic property to determine whether to fill the shape with the given colour or not. Also note that in the implementations I have added a Sleep
command, to demonstrate some heavy computation. It will become a little more clear in a while.
Let's start with our Line
class:
namespace Flyweight.Shapes;
public class Line : IShape
{
public Line()
{
Console.WriteLine("Creating a new line object");
// Simulate heavy computation
System.Threading.Thread.Sleep(1000);
}
public void Draw(Graphics g, int x1, int y1, int x2, int y2, Color color)
{
g.DrawLine(new Pen(color), x1, y1, x2, y2);
}
}
Then the Oval
class. Note the intrinsic value _fill
:
namespace Flyweight.Shapes;
public class Oval : IShape
{
private bool _fill;
public Oval(bool fill)
{
_fill = fill;
Console.WriteLine($"Creating Oval object with fill: {fill}");
// Simulate heavy computational work
System.Threading.Thread.Sleep(2000);
}
public void Draw(Graphics g, int x, int y, int width, int height, Color color)
{
g.DrawEllipse(new Pen(color), x, y, width, height);
if (_fill)
g.FillEllipse(new SolidBrush(color), x, y, width, height);
}
}
Next up, the Triangle
class:
namespace Flyweight.Shapes;
public class Triangle : IShape
{
private bool _fill;
public Triangle(bool fill)
{
_fill = fill;
Console.WriteLine($"Creating triangle with fill: {_fill}");
System.Threading.Thread.Sleep(800);
}
public void Draw(Graphics g, int x, int y, int width, int height, Color color)
{
var points = new Point[3];
points[0] = new Point(x + width / 2, y);
points[1] = new Point(x, y + height);
points[2] = new Point(x + width, y + height);
g.DrawPolygon(new Pen(color), points);
if (_fill)
{
g.FillPolygon(new SolidBrush(color), points);
}
}
}
And finally the Square
class:
namespace Flyweight.Shapes;
public class Square : IShape
{
private bool _fill;
public Square(bool fill)
{
_fill = fill;
Console.WriteLine($"Creating triangle with fill: {_fill}");
System.Threading.Thread.Sleep(800);
}
public void Draw(Graphics g, int x, int y, int width, int height, Color color)
{
g.DrawRectangle(new Pen(color), x, y, width, height);
if(_fill)
g.FillRectangle(new SolidBrush(color), x, y, width, height);
}
}
It's now time for our Flyweight Factory participant. We need some kind of registry to store the flyweights. We also want the registry to be inaccessible to the client application. For that reason, we are going to create a class that contains a Dictionary
. If the client requests a shape that exists in the registry, it will be returned. If it doesn't it will be instantiated on the spot.
Let's see our ShapeFactory
class:
using Flyweight.Shapes;
namespace Flyweight;
public class ShapeFactory
{
private static readonly Dictionary<ShapeType, IShape> shapes = new Dictionary<ShapeType, IShape>();
public static IShape GetShape(ShapeType type)
{
IShape _concreteShape = shapes.GetValueOrDefault(type, null);
if (_concreteShape != null)
return _concreteShape;
_concreteShape = type switch
{
ShapeType.LINE => new Line(),
ShapeType.OVAL_FILL => new Oval(true),
ShapeType.OVAL_NOFILL => new Oval(false),
ShapeType.SQUARE_FILL => new Square(true),
ShapeType.SQUARE_NOFILL => new Square(false),
ShapeType.TRIANGLE_FILL => new Triangle(true),
ShapeType.TRIANGLE_NOFILL => new Triangle(false),
_ => _concreteShape
} ?? throw new InvalidOperationException();
shapes.Add(type, _concreteShape);
return _concreteShape;
}
}
I have also created a ShapeType
enum, to select the appropriate shapes from the registry:
namespace Flyweight;
public enum ShapeType
{
LINE,
OVAL_NOFILL,
OVAL_FILL,
TRIANGLE_NOFILL,
TRIANGLE_FILL,
SQUARE_NOFILL,
SQUARE_FILL
}
Finally, we need something to paint. So below is the Painter
class. This class might look a bit complicated, but it just sets up a frame to draw on and then selects a combination of shape and color randomly and paints it on the screen:
namespace Flyweight;
public partial class Painter : Form
{
private readonly int WIDTH;
private readonly int HEIGHT;
private static readonly ShapeType[] shapes = { ShapeType.LINE, ShapeType.OVAL_FILL, ShapeType.OVAL_NOFILL, ShapeType.SQUARE_FILL, ShapeType.SQUARE_NOFILL, ShapeType.TRIANGLE_FILL, ShapeType.TRIANGLE_NOFILL };
private static readonly Color[] colors = { Color.Red, Color.Green, Color.Yellow, Color.Aquamarine, Color.Chartreuse, Color.Black, Color.Indigo };
public Painter(int width, int height)
{
this.WIDTH = width;
this.HEIGHT = height;
var contentPane = Controls;
var startButton = new Button { Text = "Draw" };
var panel = new Panel { Dock = DockStyle.Fill };
contentPane.Add(panel);
contentPane.Add(startButton);
startButton.Dock = DockStyle.Bottom;
Visible = true;
Size = new Size(WIDTH, HEIGHT);
FormBorderStyle = FormBorderStyle.FixedSingle;
MaximizeBox = false;
StartPosition = FormStartPosition.CenterScreen;
startButton.Click += (sender, e) =>
{
var g = panel.CreateGraphics();
for (int i = 0; i < 20; ++i)
{
var shape = ShapeFactory.GetShape(GetRandomShape());
shape.Draw(g, GetRandomX(), GetRandomY(), GetRandomWidth(),
GetRandomHeight(), GetRandomColor());
}
};
}
private ShapeType GetRandomShape()
{
var random = new Random();
return shapes[random.Next(shapes.Length)];
}
private int GetRandomX()
{
var random = new Random();
return random.Next(WIDTH);
}
private int GetRandomY()
{
var random = new Random();
return random.Next(HEIGHT);
}
private int GetRandomWidth()
{
var random = new Random();
return random.Next(WIDTH / 10);
}
private int GetRandomHeight()
{
var random = new Random();
return random.Next(HEIGHT / 10);
}
private Color GetRandomColor()
{
var random = new Random();
return colors[random.Next(colors.Length)];
}
[STAThread]
private static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new Painter(1024, 768));
}
}
If you run the project, and press the Draw
button, you will notice that it requires some time for the program to start painting. This is because we are instantiating the Flyweight objects:
If you continue to press the Draw
button you will see that the shapes appear instantaneously, and you can create something like the following abstract art from the 80s:
Pros and Cons of the Facade Pattern
✔ We can reduce the memory footprint of the application. | ❌Code complexity increases. |
✔Separate flyweights allow our application to add entities or models later without disturbance. | ❌We might be trading memory with CPU cycles if we compute the extrinsic state frequently. |
Relations with Other Patterns
- To reduce the amount of RAM that is used, the shared leaf nodes of the Composite tree can be implemented as Flyweights.
- The Flyweight demonstrates how to create a large number of small objects, whereas the Facade demonstrates how to create a single object that represents a complete subsystem.
- If it were possible to consolidate all of the objects' common states into a single Flyweight object, Flyweight would function in a manner very similar to Singleton. On the other hand, there are two significant distinctions between these patterns:
- One and only one instance of a Singleton class should exist, whereas a Flyweight class may include numerous instances, each of which may have a unique intrinsic state.
- The Singleton object may be changed in some way. Immutable things are those that have a flyweight.
Final Thoughts
In this article, we have discussed what is the Flyweight pattern, when to use it and what are the pros and cons of using this design pattern. We then examined some use cases for this pattern and how the Flyweight relates to other classic design patterns.
It's worth noting that the Flyweight pattern, along with the rest of the design patterns presented by the Gang of Four, is not a panacea or a be-all-end-all solution when designing an application. Once again it's up to the engineers to consider when to use a specific pattern. After all these patterns are useful when used as a precision tool, not a sledgehammer.
We've reached the end of our Gang of Four Design Patterns series! I hope you had as much fun reading it as I had researching and writing it. Completing this series feels like a big achievement for me since it's the first substantial series I've finished since I started blogging. Thank you so much for joining me on this journey, I appreciate your support and hope you'll stick around for more!
Posted on May 9, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.