Design Pattern: Prototype
Sometimes in development, we need to create a new object that is the same or very similar to an existing one. The most direct way is to create a new object and copy each property value from the original object.
But doing this has many problems. The Prototype Pattern is a useful solution for this situation.
Here is a simple example. We have a User
class with username and a list of permissions:
class User {
private String name;
private List<String> permissions;
public User(String name, List<String> permissions) {
this.name = name;
this.permissions = permissions;
}
}
Now we have a user called admin1
with the permissions: view, edit, and delete:
List<String> permissions = new ArrayList<>();
permissions.add("view");
permissions.add("edit");
permissions.add("delete");
User admin1 = new User("Admin", permissions);
We want to create another user, admin2
, with the same permissions as admin1
but a different username. We might write code like this:
// Manually copy admin1's permissions
List<String> admin2Permissions = new ArrayList<>(admin1.getPermissions());
User admin2 = new User("Admin2", admin2Permissions);
This manual way works, but there are some clear problems:
Code coupling: The code that creates the new object depends on the internal details of the
User
class. The client code needs to know all the properties ofUser
to copy it correctly.Hard to maintain: If the
User
class adds new properties, every place that copies aUser
object by hand needs to change. This is easy to miss and breaks the "Open-Closed Principle".
To solve these problems, we can use the Prototype Pattern. The main idea is: Don't let the caller copy the object. Let the object copy itself.
Let’s see how to change the User
creation process using the Prototype Pattern. First, we define a Cloneable
interface with a clone
method:
interface Cloneable<T> {
T clone();
}
Then, make the User
class implement this interface:
class User implements Cloneable<User> {
private String name;
private List<String> permissions;
public User(String name, List<String> permissions) {
this.name = name;
this.permissions = permissions;
}
public List<String> getPermissions() {
return permissions;
}
public void setPermissions(List<String> permissions) {
this.permissions = permissions;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public User clone() {
// Create a new permissions list and copy all elements
// This is deep copy, making sure the original and copy are separated
// Otherwise, they may share the same list and change each other by mistake
List<String> clonedPermissions = new ArrayList<>(this.permissions);
return new User(this.name, clonedPermissions);
}
}
Now, when we need a copy of a User
object, we just call its clone()
method:
// Directly copy the object using clone()
// No need to worry about User's inner details
User newUser = admin1.clone();
newUser.setName("newUser");
// Changing newUser will not affect admin1
newUser.getPermissions().add("publish");
In this way, the caller does not need to know the details of User
. No matter how the fields change in the future, as long as the clone()
method copies things correctly, other code does not need to change. This reduces coupling.
There are three key roles in the Prototype Pattern:
- Prototype: Declares an interface for cloning itself, which is our custom
Cloneable
interface. - Concrete Prototype: The class that implements the clone interface, like the
User
class above. - Client: The code that calls
clone()
to get a new object.
Prototype Manager
In some situations, we need to manage a series of prototype objects that can be cloned. At this time, we can use a Prototype Manager or Prototype Registry.
The Prototype Manager is a place to store and retrieve prototype objects. The client does not work with prototypes directly. Instead, it asks the manager for a clone by a unique key, such as a string name.
Let's still use the User
class as an example. We create a prototype manager:
class PrototypeManager {
private Map<String, User> prototypes = new HashMap<>();
public void addPrototype(String key, User user) {
prototypes.put(key, user);
}
public User getPrototype(String key) {
User prototype = prototypes.get(key);
if (prototype != null) {
return prototype.clone();
}
return null;
}
}
Now, register some preset user types to the prototype manager:
// 1. Create a prototype manager
PrototypeManager manager = new PrototypeManager();
// 2. Create and register prototype objects
User readonly = new User("readonly", Arrays.asList("view"));
User admin = new User("admin", Arrays.asList("view", "edit", "delete"));
User superAdmin = new User("superAdmin", Arrays.asList("view", "edit", "delete", "publish"));
manager.addPrototype("readonly", readonly);
manager.addPrototype("admin", admin);
manager.addPrototype("superAdmin", superAdmin);
When you need to make a new user, just request it from the manager:
// Create a normal user
User user1 = manager.getPrototype("readonly");
user1.setName("John");
// Create an admin user
User user2 = manager.getPrototype("admin");
user2.setName("Tom");
// Create a super admin user
User user3 = manager.getPrototype("superAdmin");
user3.setName("Admin");
The prototype manager is very useful in systems where you need to configure or manage many kinds of prototype objects. It separates object creation from usage. This makes the system more flexible and easier to extend.
Summary
Main advantages of the prototype pattern:
- Decoupling: The client code does not care about the exact class. It just needs to know how to clone an object, not how to create it from scratch.
- Simplified object creation: For complex objects, cloning an existing object is much easier than creating a new one from the beginning.
- Performance improvement: If creating an object is slow (for example, needs to access a database or network), cloning speeds things up a lot.
Main drawback of the prototype pattern:
Writing a correct and deep clone()
method for every class can be hard, especially if your objects have nested objects or circular references. You need to be careful.
Overall, if you often create objects that are similar to existing ones, or the creation cost is high, the prototype pattern is a good design pattern to use.