Hello everyone, and welcome to .NET Core Central. In this blog post, I will walk through the flyweight design pattern. The flyweight design pattern is one of the design patterns from the Gang of Four design patterns.
The intent of the Flyweight Pattern
The flyweight design pattern is a structural design pattern. And the main intent of this pattern is to use sharing, to support a large number of fine-grained objects efficiently.
To elaborate, if you have an application where you use the same object multiple times, then instead of creating the object every single time you can share the object across multiple operations. This will help you get efficiency both in terms of memory as well as CPU.
Use-Case for the Flyweight Pattern
The flyweight design pattern is extremely important when you are building a client-side application. Especially if you are building a mobile application using C# and Xamarin. Or if you are building a web-based application in some other programming languages.
When it comes to server-side programming, we are usually not very conscious about memory and CPU utilization for every application. It’s because the object lifetime of web-based applications is anyway small.
And the reason is that you can declare something as scoped to a particular HTTP request. And during the request workflow, the object will go out of memory as soon as the request is complete.
The other reason is the memory on the server is cheap and easily available.
But both of the above-mentioned scenarios are not valid for client applications (e.g., an application running on a mobile device).
But nonetheless, this is a very important design pattern, and we should use it where it makes sense, regardless of client or server.
Advantage
If you are creating an object just five or six times, in that case, I think it can be overkill to use the flyweight design pattern. But imagine a scenario where you are creating hundreds of the same objects again and again. In those scenarios, the flyweight design pattern will be extremely handy.
An example of the Flyweight Pattern
For this blog post, we are going to walk through a scenario where there is an application that will be showing the location of cars. And consider that this application is a distributed application and it keeps all the car’s information in memory on some sort of server.
If hundreds or thousands of users use this application, creating a car object every single time will take a lot of memory. And that is the case where the flyweight design pattern will be applicable.
The code
For this example, I will be using Visual Studio 2022, .NET 6 framework, and C# programming language.
Firstly, we will create an interface name ICar
. This interface will have two properties, Color
and Engine
, which will represent the color and the engine of the car. It will also have a method SetLocation
, which will take two parameters, lat
for latitude and lon
for longitude of the location of the car. This method is for setting the location of the car.
Considering intrinsic vs extrinsic data
The implementation of this interface will have two different states.
The first one is a static state, which is the color and engine of the car. And once we set these two values, it is not going to change throughout the lifetime of a particular instance. Which is the intrinsic data, as it is the information that does not change based on context.
Whereas the location is the dynamic state. Which is the latitude and longitude of where the car currently is. And it is going to change based on where the car goes. Which is the extrinsic data, as it is the information that changes based on context.
So when you design with the flyweight design pattern, this is what we have to segregate, the intrinsic data and the extrinsic data.
In the flyweight design, the class or the interface which saves the intrinsic data is the flyweight. And in our case, it is the class that will derive from ICar is the flyweight.
namespace FlyweightPattern.Demo;
public interface ICar
{
string Color { get; }
string Engine { get; }
void SetLocation(decimal lat, decimal lon);
}
One important point to keep in mind as I mentioned earlier as well, is that if your class just has two or three properties, which means it’s anyway very lightweight, a flyweight pattern might be an overkill. We should use the flyweight pattern only in case of objects which are heavy and they are expensive both in terms of memory and CPU during creation.
Note:
For the simplicity of the example, I have kept this class lightweight with only 2 properties and one method.
Classes derived from ICar
We will define two classes that will derive from the ICar
interface.
The first one is a BmwCar
, and the constructor of this class will take two string parameters, engine, and color. And inside the constructor, it will set the Engine
and Color
properties with the parameters.
And the SetLocation
method, which had the latitude and longitude as the parameter will just print out the parameters along with the car’s engine and color information.
namespace FlyweightPattern.Demo;
public class BmwCar : ICar
{
public string Color {get; private set;}
public string Engine {get; private set;}
public BmwCar(string color, string engine)
{
Color = color;
Engine = engine;
}
public void SetLocation(decimal lat, decimal lon)
{
Console.WriteLine($"Current location of BMW Car with color: {Color}, Engine: {Engine} is {lat}, {lon}");
}
}
Similarly, we will have another implementation of ICar
, and it will be AudoCar
. The implementation of AudiCar
will be the same as the BmwCar
.
namespace FlyweightPattern.Demo;
public class AudiCar : ICar
{
public string Color { get; private set; }
public string Engine { get; private set; }
public AudiCar(string color, string engine)
{
Color = color;
Engine = engine;
}
public void SetLocation(decimal lat, decimal lon)
{
Console.WriteLine($"Current location of Audi Car with color: {Color}, Engine: {Engine} is {lat}, {lon}");
}
}
The idea here is that all the BMW cars will share the same car instance instead of creating a new BMW car instance every time. So when someone has a BMW car and they want to see where their car is, the only variable will be the latitude and longitude of the car. And we will manage these two values can from the outside and will have different values for different customers.
Managing a single instance through a factory
How do we ensure that there is only one instance of car class? There are a couple of ways to do it, the traditional way of doing it is through a factory. And that is what we will use here.
For the factory, we will create a CarFactory
class, that will implement the ICarFactory
interface. This class will have one public method GetCar
, which will return the instance of ICar
based on the car type passed.
When someone calls GetCar
, it checks if the car exists in a dictionary and then returns, and if not, it just creates a car and then adds it to the dictionary, and then returns. This will ensure that we create a type of car only once.
namespace FlyweightPattern.Demo;
public interface ICarFactroy
{
ICar GetCar(string type);
}
public class CarFactroy : ICarFactroy
{
private readonly IDictionary<string, ICar> cars = new Dictionary<string, ICar>();
public ICar GetCar(string type)
{
if (cars.ContainsKey(type)) return cars[type];
var car = CreateCar(type);
cars.Add(type, car);
return car;
}
private ICar CreateCar(string type) =>
type switch
{
"Bmw" => new BmwCar("V8", "Red"),
"Audi" => new AudiCar("V6", "Blue"),
_ => throw new ArgumentException("Invalid Choice")
};
}
Managing car state
Finally, we need some sort of manager for the car, which is going to manage the car itself and maintain the extrinsic data or the contextual data. In our case, the contextual data is the latitude and the longitude.
So for that, we can create a new class CarManager
. The constructor of the CarManager
class is going to take the ICarFactory
interface and string type
as parameters.
And inside the constructor, based on the type of the car, using the ICarFactor
it will get an instance of the car.
The next method of CarManager
is SetLocation
, which will take the latitude and longitude as parameters. And inside this method, we will just call the SetLocation
of the car object passing the parameters.
namespace FlyweightPattern.Demo;
public interface ICarManager
{
void SetLocation(decimal lat, decimal lon);
}
public class CarManager : ICarManager
{
private readonly ICar car;
public CarManager(ICarFactroy carFactroy, string type)
{
car = carFactroy.GetCar(type);
}
public void SetLocation(decimal lat, decimal lon)
{
car.SetLocation(lat, lon);
}
}
And here as you can see, the CarManager abstracts access to the flyweight object which is the ICar
object here. So when someone wants to interact with the car, they will always deal with the CarManager
, which will manage the state of the cars.
So if someone wants to create three Audi cars for three of their customer, they will just create three instances of CarManager
. Which internally will create only one instance of AudiCar
, but will manage the state of the car.
Conclusion
A flyweight pattern is very useful when we have concerns about memory consumption due to large objects. Without this requirement, in most cases, the flyweight design pattern is not very useful.
A Youtube video for this implementation is available here.