Suppose you are playing a game that has been created using the principle of object-oriented programming.
Now, if there are n number of players currently in the game, the program will have to create n number of objects. Due to this, at some point, the RAM may run out, causing your game to crash.
A solution would be to take the common features of the player and group them together into a reusable shared object. So, the shared object will only carry the character’s code. However, features extrinsic to the character, for instance, the name given by the user and their co-ordinate position in the game, can be introduced separately.
This solution is known as the Flyweight Design Pattern.
A Flyweight Design Pattern describes how to use the objects efficiently to reduce the cost of operations. It creates a shared object, also known as a flyweight object, which carries immutable information about the object.
In this way, a single object can carry a pool of flyweights to reduce the overall storage costs.
In this article, you will learn about the Flyweight Design Pattern and its ability to use the object to the finest of its granularity, along with real-world examples in C#.
What is the Flyweight design pattern?
Design Patterns are standard methods to easily resolve recurring problems. Out of the 23 patterns introduced by the Gang of Four (developers who founded these solutions), the Flyweight design pattern falls in the category of a structural pattern.
A structural design pattern defines how objects and classes work together to form larger structures or gain flexibility. The Flyweight pattern, in particular, describes how developers can create many objects without running out of memory space.
A Flyweight pattern introduces the concept of the intrinsic and extrinsic state of the object. Take the example of a document editor. If a user enters the word “paper”, each letter in the word is an object. However, instead of creating duplicate objects for ‘p’, the editor can create a shared object that is utilized every time you call that character.
Moreover, the object carries the code of the character only, known as its intrinsic (immutable) property. However, each letter will appear at a different position in the document, which will be its extrinsic (mutable) property. So, instead of creating a single object for each character in the document, the editor uses flyweights and extrinsic properties passed as parameters.
When to use the Flyweight design pattern?
The Flyweight pattern is space efficient only when you use it appropriately.
Use the Flyweight pattern if all of the following are true:
- There is a lot of objects used in the application.
- Storage costs are unreasonably high due to the number of objects.
- The majority of objects can have extrinsic states.
- By removing extrinsic states, multiple objects can be grouped into a single object.
- Object identity does not play any role in your application since the shared objects might give true results for identity tests.
How to implement the Flyweight pattern in C# – Structural code
The UML participants of the Flyweight pattern are:
- Flyweight – declares the interface through which flyweights operate in their extrinsic state.
- ConcreteFlyweight – carries the implementation details of the Flyweight. It must be shareable with an intrinsic state and non-dependency on the context.
- UnsharedConcreteFlyweight (optional) – Flyweight does not impose sharing. There can be unshared Flyweights that have Concrete Flyweights as children at some part of the code structure.
- FlyweightFactory – creates a flyweight object if it doesn’t exist or supplies the existing instance to the client when requested.
- Client – carries and computes the extrinsic states of flyweights.
Here is the structural C# code for the Flyweight design pattern.
public interface Flyweight
{
void Operation(string ExState);
}
public class ConcreteFlyweight : Flyweight
{
public string ExtrinsicState { get; set; }
public void Operation(string ExState)
{
Console.WriteLine(" ConcreteFlyweight: " + ExState);
}
}
public class FlyweightFactory
{
private static Dictionary<string, Flyweight> _flyweights = new();
public FlyweightFactory()
{
_flyweights.Add("X", new ConcreteFlyweight());
_flyweights.Add("Y", new ConcreteFlyweight());
_flyweights.Add("Z", new ConcreteFlyweight());
}
public Flyweight GetFlyweight(string key)
{
return _flyweights[key];
}
}
And the usage:
FlyweightFactory factory = new FlyweightFactory();
// Work with different flyweight instances
Flyweight F = factory.GetFlyweight("X");
F.Operation("Extrinsic State of the Flyweight X");
Flyweight Y = factory.GetFlyweight("Y");
Y.Operation("Extrinsic State of the Flyweight Y");
Flyweight X = factory.GetFlyweight("X");
F.Operation("Extrinsic State of the Flyweight XNEW");
The result is:
ConcreteFlyweight: Extrinsic State of the Flyweight X
ConcreteFlyweight: Extrinsic State of the Flyweight Y
ConcreteFlyweight: Extrinsic State of the Flyweight XNEW
In this example, the code uses the Flyweight interface with a single-method Operation. The Concrete Flyweight is the shared object which accepts the extrinsic state for operations. The Flyweight factory creates flyweights if not present, either “X, Y or Z”, or returns the flyweights if present.
The client then calls the flyweights and passes the extrinsic states for operation. This is how the flyweight structure works.
Flyweight pattern – Real-world example in C#
In this example, the Flyweight pattern draws different shapes. Each shape has its own intrinsic properties: height, width, and name. However, every circle drawn will have a different position. So, the position comes as an extrinsic element.
The client inputs the extrinsic state to the factory, generating new instances of the different shapes.
public class DrawingFactory
{
private Dictionary<string, Shape> shapes = new();
Shape? shape = null;
public Shape GetShape(string key)
{
if (shapes.ContainsKey(key))
{
shape = shapes[key];
}
else
{
switch (key)
{
case "Triangle": shape = new Triangle(); break;
case "Square": shape = new Square(); break;
case "Rectangle": shape = new Rectangle(); break;
}
shapes.Add(key, shape);
}
return shape;
}
}
/// <summary>
/// The 'Flyweight' abstract class
/// </summary>
public abstract class Shape
{
protected string _name = string.Empty;
protected int _width;
protected int _height;
protected int _position;
public abstract void Display(int position);
}
/// <summary>
/// A 'ConcreteFlyweight' class
/// </summary>
public class Triangle : Shape
{
// Constructor
public Triangle()
{
_name = "Triangle";
_height = 100;
_width = 120;
}
public override void Display(int position)
{
this._position = position;
Console.WriteLine(_name +
" (position " + this._position + ")");
}
}
/// <summary>
/// A 'ConcreteFlyweight' class
/// </summary>
public class Square : Shape
{
// Constructor
public Square()
{
_name = "Square";
_height = 100;
_width = 120;
}
public override void Display(int position)
{
this._position = position;
Console.WriteLine(_name +
" (position " + this._position + ")");
}
}
/// <summary>
/// A 'ConcreteFlyweight' class
/// </summary>
public class Rectangle : Shape
{
public Rectangle()
{
_name = "Square";
_height = 100;
_width = 120;
}
public override void Display(int position)
{
this._position = position;
Console.WriteLine(_name +
" (position " + this._position + ")");
}
}
And the usage is:
string[] draw = {
"Triangle",
"Rectangle",
"Square",
"Triangle"
};
DrawingFactory factory = new();
// extrinsic state
int position = 100;
foreach (string drawing in draw)
{
position++;
Shape shape = factory.GetShape(drawing);
shape.Display(position);
}
Console.ReadKey();
The results are:
Triangle (position 101)
Square (position 102)
Square (position 103)
Triangle (position 104)
In this way, instead of creating a new instance each time a new shape was required, we utilized the shared objects multiple times to conserve space.
Benefits and drawbacks of the Flyweight pattern
The Flyweight pattern resolves the problem of dealing with an ever-increasing number of objects. However, like any solution, it has its own drawbacks, and therefore, you should use it carefully.
Here are the benefits and drawbacks of the flyweight pattern:
Benefits:
- The Flyweight design pattern reduces the number of objects by creating shared objects (flyweights).
- Having separate flyweights provides the flexibility of increasing the entities or models in your application at a later stage without any disruption, for instance, increasing the number of shapes that your application produces.
Drawbacks:
- Computing and passing the extrinsic state might add to the load on the CPU.
- This pattern may make the code unnecessarily complex.
- The more shared flyweights, the greater the storage savings. Using this pattern inappropriately can increase the computations without benefiting in any way.
What are the related patterns to the Flyweight design pattern?
The Flyweight pattern with the Composite pattern creates a graph having shared leaf nodes, where the parent pointer is passed as an extrinsic state. Since the nodes cannot store a pointer to their parent nodes, this is an extremely useful method to enhance communication.
It is a good practice often to implement State and Strategy pattern objects as shared flyweights.
Conclusion
The Flyweight design pattern divides the objects into intrinsic and extrinsic states to create shared objects. This saves the RAM being used at run time.
The pattern is useful anywhere when you have a lot of objects in memory, and a more optimal usage is required.
In this article, you learned about the structure, the real-world example of the Flyweight Pattern, and its pros and cons!
If you want to learn more about design patterns, check the separate in-depth article about design patterns in C#.