How to Implement Design Patterns in TypeScript: Prototype
What is the Prototype Design Pattern?
The Prototype design pattern is one of the creational design patterns, which involves creating objects based on a template of an existing object through cloning. Instead of creating a new instance of an object from scratch and configuring it, the prototype pattern allows you to copy an existing object and possibly customize it.
Intent:
- Specify the kinds of objects to create using a prototypical instance and create new objects by copying this prototype.
Applicability:
- When the classes to instantiate are specified at run-time, for example, by dynamic loading.
- To avoid building a class hierarchy of factories that parallels the class hierarchy of products.
- When instances of a class can have one of only a few different combinations of state.
Advantages:
- Reduced subclassing: The prototype pattern helps reduce the number of named classes the system needs by letting you clone a prototype instead of calling a factory method.
- Dynamic configuration of an application with classes: Through the use of prototypes, you can instantiate and configure objects dynamically.
- Performance: For instance, in scenarios where creating a brand new instance is more expensive than copying an existing one, using the prototype can be more efficient.
Drawbacks:
- Deep copying objects can be complex, especially if the object’s internal references lead to cyclical structures.
- Not all objects can be cloned easily depending on their encapsulation constraints and internal state.
How to Implement Prototype?
Let’s consider a real-world example where the Prototype design pattern could be useful: managing graphic shapes in a drawing application.
Imagine you’re creating a drawing app, and users can place shapes like circles, squares, and lines on a canvas. Once a shape is added, users may want to create duplicates of that shape with the same properties but move or adjust them individually.
abstract class Shape {
x: number;
y: number;
color: string;
constructor(source?: Shape) {
if (source) {
this.x = source.x;
this.y = source.y;
this.color = source.color;
}
}
abstract clone(): Shape;
}
class Circle extends Shape {
radius: number;
constructor(source?: Circle) {
super(source);
this.radius = source ? source.radius : 0;
}
clone(): Circle {
return new Circle(this);
}
}
class Rectangle extends Shape {
width: number;
height: number;
constructor(source?: Rectangle) {
super(source);
this.width = source ? source.width : 0;
this.height = source ? source.height : 0;
}
clone(): Rectangle {
return new Rectangle(this);
}
}
Now, let’s use these classes:
// Create an instance of Circle
let circle: Circle = new Circle();
circle.x = 10;
circle.y = 10;
circle.radius = 20;
circle.color = "red";
// Clone the circle
let anotherCircle: Circle = circle.clone();
anotherCircle.x = 20; // move the cloned circle to a different position
// Create an instance of Rectangle
let rectangle: Rectangle = new Rectangle();
rectangle.x = 5;
rectangle.y = 5;
rectangle.width = 50;
rectangle.height = 30;
rectangle.color = "blue";
// Clone the rectangle
let anotherRectangle: Rectangle = rectangle.clone();
anotherRectangle.width = 60; // change the width of the cloned rectangle
// At this point, we have original and cloned shapes with some properties shared and some changed.
In this example, the Prototype pattern allows for easy and efficient creation of new shape objects that carry the properties of existing shapes but can be individually modified.
When considering the prototype pattern, it’s essential to evaluate the pros and cons in the context of the specific requirements and complexities of the application at hand.