Flutter Design Patterns: 14 — Prototype
An overview of the Prototype design pattern and its implementation in Dart and Flutter
Previously in the series, I have analyzed one of the behavioural design patterns — Memento. This time I would like to represent a relatively simple design pattern comparing to the other patterns in the series which belongs to the category of creational design patterns — the Prototype.
Table of Contents
- What is the Prototype design pattern?
- Other articles in this series
- Your contribution
What is the Prototype design pattern?
The Prototype is a creational design pattern, which intention in the GoF book is described like this:
Specify the kinds of objects to create using a prototypical instance, and create new objects by copying this prototype.
That is, instead of creating a new object, some prototype is used which allows creating new objects by copying from this prototype. This could be useful when you want to copy an object which has a complicated state, which means that just initialising a new object will not be enough, you also need to reach that particular state of the object to consider it as a valid copy. In simple words, when your code only wants a copy of a specific object with the exact same state and you do not care about the details on how this state was reached — well, your code should just provide a way to copy (clone) it.
In general, an inflexible way is to create an object directly within the class that requires (uses) the object. The Prototype design pattern enables the run-time flexibility since a class can be configured with different Prototype objects, which are copied to create new objects, and even more, Prototype objects can be added and removed dynamically at run-time.
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 Prototype design pattern looks like this:
- Prototype — declares an interface for cloning itself. Usually, it is a single clone method, but there could be other methods declared/defined if needed;
- ConcretePrototype — implements an operation for cloning itself. In addition to copying the original object’s data to the clone, this method may also handle some edge cases of the cloning process related to cloning linked objects, untangling recursive dependencies, etc.;
- SubclassPrototype — has the same purpose as the ConcretePrototype, but could also extend the base class by defining additional properties, behaviour, etc.;
- Client — creates a new object by asking a prototype to clone itself.
The Prototype design pattern should be used when your code should not depend on the concrete classes of objects that you need to copy. The Prototype pattern provides the client code with a general interface for working with all objects that support cloning. This interface makes the client code independent from the concrete classes of objects that it clones.
Also, the pattern could be used when you want to reduce the number of subclasses that only differ in the way they initialize their respective objects. Instead of instantiating a subclass that matches some configuration, the client can simply look for an appropriate prototype and clone it.
Finally, the pattern is quite useful when you have a set of pre-built objects that are ready to be copied. These objects could be stored inside some kind of prototype registry from which you can access the frequently-used prototypes. In this way, you can instantiate a dynamically loaded class and use it inside your code.
This time, the implementation part of the design pattern is very simple and straightforward (but that could be also a good sign, right?). Let’s say there are multiple shapes in your application which should be copied at run-time and provided to the UI. Of course, it is possible to instantiate a specific shape by checking all the properties and using them to just simply create another object. However, different shapes contain different properties, they are of a different type, hence the logic just to copy a specific shape could lead to a cumbersome mess in the application code base which you probably want to avoid.
Wouldn’t it be nice to have a unified way to copy any shape in your application without even considering any details about it in your client code? Well, we have just analysed the Prototype design pattern, maybe give it a try?
The class diagram below shows the implementation of the Prototype design pattern:
Shape is an abstract class which is used as a base class for all the specific shapes. The class contains a color property and defines several abstract methods:
- clone() — an abstract method to clone (copy) the specific shape;
- randomiseProperties() — an abstract method to randomise property values of the shape;
- render() — an abstract method to render the shape. The method is used in UI.
Circle and Rectangle are concrete shape classes which extend the abstract class Shape and implement its abstract methods.
PrototypeExample initializes and contains several Shape objects. These objects are rendered in the UI using the render() method. Also, specific shape objects could be copied using clone() and their properties could be randomised using randomiseProperties() methods respectively.
An abstract class which stores the shape’s colour and defines several abstract methods. Also, this class contains several constructors:
- Shape() — a basic constructor to create a shape object with the provided colour value;
- Shape.clone() — a named constructor to create a shape object as a copy of the provided Shape value.
Circle — a specific class which defines a shape of a circle. The class defines a radius property, extends the Shape class and implements its abstract methods clone(), randomiseProperties() and render().
Rectangle — a specific class which defines a shape of a rectangle. The class defines height and width properties, extends the Shape class and implements its abstract methods clone(), randomiseProperties() and render().
First of all, a markdown file is prepared and provided as a pattern’s description:
PrototypeExample contains a couple of Shape objects — Circle and Rectangle. By pressing the Randomise properties button, the values of shape’s properties are randomised (the randomiseProperties() method is called on the shape). Also, if the Clone button is pressed, the clone() method is called on the shape and a copy of that particular shape is crated with the exact same values of all the properties.
The PrototypeExample does not care about the specific type of shape object as long as it extends the Shape abstract class and implements all of its abstract methods. As a result, the clone() method could be called on any shape, all of its properties are copied even though these are different on different shapes e.g. the circle has only the radius property which is specific for that particular shape, while the rectangle has two different properties — height and width.
As you can see in the example, it does not matter of which type the specific shape is. As long as it extends the prototype base class defining the clone() method, the shape object could be copied at any time and used across your code — whether it is your business logic or UI, it is irrelevant.
All of the code changes for the Prototype design pattern and its example implementation could be found here.
👏 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.