Flutter Design Patterns: 19 — Flyweight
An overview of the Flyweight design pattern and its implementation in Dart and Flutter
In the last article, I have analysed a creational design pattern which divides the construction of a complex object into several separate steps — Builder. In this article, I would like to analyse and implement a structural design pattern that helps using a huge number of objects in your code which could barely fit into available RAM — it is Flyweight.
Table of Contents
- What is the Flyweight design pattern?
- Other articles in this series
- Your contribution
What is the Flyweight design pattern?
Flyweight belongs to the category of structural design patterns. The intention of this design pattern is described in the GoF book:
Use sharing to support large numbers of fine-grained objects efficiently.
Let’s take an object-oriented document editor as an example. For document elements like tables, images or figures, separate objects are created. However, for text elements (individual characters) this is not feasible even though it promotes flexibility: characters and any other embedded elements could be treated uniformly, the application could be extended to support new character sets very easily. The reason is simple — limitations of the hardware. Usually, a document contains hundreds of thousands of character objects which would consume a lot of memory and it could lead to unexpected crashes, for instance, when the document is being edited and eventually there would be no memory left for new characters or any other type of embedded objects. How this kind of object-oriented document editors could be implemented, then?
The “secret” relies on the flyweight objects — shared objects that can be used in multiple contexts simultaneously. But how this could even work? If we reuse the same object, doesn’t that mean that when the object is changed in one place, all the other places are affected, too? Well, the key concept here is the distinction between intrinsic and extrinsic state. The intrinsic state is invariant (context-independent) and therefore can be shared e.g. the code of a character in the used character set. The extrinsic state is variant (context-dependent) and therefore can not be shared e.g. the position of a character in the document. And that’s the reason why this concept works in object-oriented editors — a separate flyweight object is created for each character in the set (e.g. each letter in the alphabet) which stores the character code as the intrinsic state, while the coordinate positions in the document of that character are passed to the flyweight object as an extrinsic state. As a result, only one flyweight object instance per character could be stored in the memory and shared across different contexts in the document structure. Sharing is caring, right?
Let’s move to the analysis and implementation parts to understand and learn the details about this pattern and how to implement it!
The general structure of the Flyweight design pattern looks like this:
- Flyweight — contains intrinsic state while the extrinsic state is passed to the flyweight’s methods. The object must be shareable (can be used in many different contexts);
- FlyweightFactory — creates and manages flyweight objects. When a client calls the factory, it checks whether the specific flyweight object exists. If yes, it is simply returned to the client, otherwise, a new instance of the flyweight object is created and then returned;
- Context — contains the extrinsic state, unique across all original objects;
- Client — computes or stores the extrinsic state of flyweight(s) and maintains a reference to it/them.
The Flyweight design pattern should be used only when your program must support a huge number of objects which barely fit into available RAM. The pattern’s effectiveness depends on how and where it’s used. It would be the most useful when:
- An application uses a large number of objects;
- The objects drain all available RAM on a target device;
- The objects contain duplicate states which can be extracted and shared between multiple objects;
- Many groups of objects could be replaced by a few shared objects once the extrinsic state is removed;
- The application doesn’t depend on object identity. Since flyweight objects are shared, conceptually distinct objects could be considered as the same object.
Sadly, the implementation would not resolve any real-world problem this time, but we will implement a simple representation screen and later investigate how the usage of the Flyweight design pattern reduces memory consumption.
Let’s say, we want to draw our custom background using two different geometric shapes — circles and squares. Also, in the background, we want to put a total of 1000 shapes at random positions. This will be implemented in two different ways:
- A new shape object would be created for each shape in the background;
- A flyweight factory would be used which creates a single object per shape.
Later, we will use a profiler tool for Dart Apps — Observatory — to investigate how much memory is used for each of these implementations. Let’s check the class diagram first and then implement the pattern.
The class diagram below shows the implementation of the Flyweight design pattern:
The ShapeType is an enumerator class defining possible shape types — Circle and Square.
The IPositionedShape is an abstract class which is used as an interface for the specific shape classes:
- render() — renders the shape — returns the positioned shape widget. Also, the extrinsic state (x and y coordinates) are passed to this method to render the shape in the exact position.
Circle and Square are concrete positioned shape classes which implement the abstract class IPositionedShape. Both of these shapes have their own intrinsic state: circle defines color and diameter properties while square contains color, width properties and a getter height which returns the same value as width.
The ShapeFactory is a simple factory class which creates and returns a specific shape object via the createShape() method by providing the ShapeType.
The ShapeFlyweightFactory is a flyweight factory which contains a map of flyweight objects — shapesMap. When the concrete flyweight is requested via the getShape() method, the flyweight factory checks whether it exists in the map and returns it from there. Otherwise, a new instance of the shape is created using the ShapeFactory and persisted in the map object for further usage.
The FlyweightExample initialises and contains the ShapeFlyweightFactory object. Also, it contains a list of positioned shape — shapesList — which is built using the ShapeFlyweightFactory and flyweight positioned shape objects.
A special kind of class — enumeration — to define different shape types.
An interface which defines the render() method to be implemented by concrete shape classes. Dart language does not support the interface as a class type, so we define an interface by creating an abstract class and providing a method header (name, return type, parameters) without the default implementation.
- Circle — a specific implementation of the IPositionedShape interface representing the shape of a circle.
- Square — a specific implementation of the IPositionedShape interface representing the shape of a square.
A simple factory class which defines the createShape() method to create a concrete shape by providing its type.
A flyweight factory class which keeps track of all the flyweight objects and creates them if needed.
First of all, a markdown file is prepared and provided as a pattern’s description:
FlyweightExample initialises and contains the ShapeFlyweightFactory class object. Also, for demonstration purposes, the ShapeFactory object is initialised here, too. Based on the selected option, either the ShapeFactory or ShapeFlyweightFactory is used to populate a list of IPositionedShape objects which are rendered in the background of the example screen.
With the ShapeFlyweightFactory, client — FlyweightExample widget — does not care about the flyweight objects’ creation or management. IPositionedShape objects are requested from the factory by passing the ShapeType, flyweight factory keeps all the instances of the needed shapes itself, only returns references to them. Hence, only a single instance of a shape object per type could be created and reused when needed.
From the example, we could see that either 2 or 1000 shape instances are created to build the screen background. However, to understand what is happening under the hood, we can check the memory consumption using Dart Observatory.
When we access the Markdown screen of the Flyweight design pattern, Circle and Square instances are not created since they are not visible on the screen:
Before rendering the Example screen (without using flyweight factory), a total of 1000 shape instances are created — in this case, 484 circles and 516 squares:
When we use the flyweight factory, only one instance per specific shape is needed which is initiated and then shared (reused) later:
One shape instance uses 16 bytes of memory, so when we initiate 1000 shapes, that is ~16kB of memory in total. However, when a flyweight factory is used, only 32 bytes are enough to store all different shape instances — 500 times less memory is needed! Now, if you increase the number of shapes to 1 million, without the flyweight factory you would need ~15.2MB of memory to store them, but with flyweight factory, the same 32 bytes would be enough.
All of the code changes for the Flyweight design pattern and its example implementation could be found here.
Other articles in this series
👏 Press the clap button below to show your support and motivate me to write better!
💬 Leave a response to this article by providing your insights, comments or wishes for the series.
📢 Share this article with your friends, colleagues in social media.
➕ Follow me on Medium.
⭐ Star the Github repository.