Sometimes you need to create a copy, or clone, of an existing object. For mutable objects with properties that can change, you may need a separate copy of an object to avoid corrupting the original. For immutable objects, where properties can be initialized but never altered, especially those with lengthy or costly initialization routines (e.g. network calls, etc.), making a copy might be more efficient than creating a new instance. These can be essential skills when working with Dart or Flutter applications.
About creational design patterns: These patterns, as the name implies, help us with the creation of objects and processes related to creating objects. With these techniques in your arsenal, you can code faster, create more flexible, reusable object templates, and sculpt a universally recognizable structure for your projects. The creational design patterns are blueprints you can follow to reliably tackle object creation issues that arise for many software projects.
The Prototype pattern is all about making an object responsible for its own cloning. Code outside an object can make a copy by creating an empty new instance and copying each property over one at a time, but what if the object has private properties? If an object includes its own cloning method, private properties won't be missed, and only the object itself needs to be aware of its internal structure.
The code for this article was tested with Dart 2.8.4 and Flutter 1.17.5.
First, let's look at how this is done with objects whose properties are open to change.
Cloning mutable objects
This is how you might create a copy of a mutable object that is not able to clone itself in the Dart language:
class Point {
int x;
int y;
Point([this.x, this.y]);
}
final p1 = Point(5, 8);
final p2 = Point(p1.x, p1.y);
final p3 = Point()
..x = p1.x
..y = p1.y;
The Point class has two public, mutable properties, x
and y
. With such a small, simple class, it's trivial to produce copies of p1
either with the class's constructor or by setting the properties on an uninitialized new object with Dart's cascade operator (..
). The big downside to this approach is that our app code is now tightly coupled to the Point class, requiring knowledge of its inner workings to produce a copy. Any changes to Point mean that app code, possibly in many places, will need matching changes, a tedious and error-prone scenario.
The Prototype pattern dictates that objects should be responsible for their own cloning, like so:
class Point {
int x;
int y;
Point([this.x, this.y]);
Point clone() => Point(x, y);
}
final p1 = Point(5, 8);
final p2 = p1.clone();
This is much cleaner, and now the app code won't need to be changed even if Point gets new or different properties in the future, as clone()
will always return a new instance of Point with the same values.
Are there any differences when working with immutable objects?
Cloning immutable objects
The same technique works fine even when we make Point immutable:
class Point {
final int x;
final int y;
const Point(this.x, this.y);
Point clone() => Point(x, y);
}
final p1 = Point(5, 8);
final p2 = p1.clone();
In this version, the constructor parameters are not optional, and the class's member variables can't be updated once initialized. This does not affect our ability to make clones. However, this class does not have a good way to modify only one or the other of the properties.
In Immutable Data Patterns in Dart and Flutter, you can see that adding a copyWith()
method gives us more flexibility with immutable objects:
class Point {
final int x;
final int y;
const Point(this.x, this.y);
Point copyWith({int x, int y}) {
return Point(
x ?? this.x,
y ?? this.y,
);
}
Point clone() => copyWith(x: x, y: y);
}
final p1 = Point(5, 8);
final p2 = p1.clone();
Here, the copyWith()
method allows you to create a new Point from an existing one while changing only individual properties. Also, the clone()
method can use it to produce a full object copy, preventing us from having to define a separate process for cloning.
Conclusion
The Prototype pattern is used extensively in the Flutter framework, particularly when manipulating themes, so a working familiarity with it will serve you well. Its basic philosophy is that an object itself is in the best position to produce its own clones, having full access to all its properties and internal workings. The pattern also prevents external code from needing detailed knowledge of an object's implementation, keeping coupling loose.
To read more about creational design patterns in Dart, check out these related articles: