Prototype Pattern Real World Implementation
Reading UML Diagram
- (-) Sign means that that member is private
- (+) Sign means that that member is public
- (Underlined Members) Means that that member is static
- «abstract» Means that that class is an abstract class
- «interface» Represents an interface
In graphic editing applications or drawing software, users can create, modify, and clone shapes. The Prototype pattern is very useful in this scenario, as it allows the program to clone complex shapes with their own internal data structure without knowing the details of their implementation.
Prototype Pattern in TypeScript
Here's the TypeScript example of the Prototype Pattern applied to the Shape object in a graphic editor:
// Define an interface for the shape's properties
interface ShapeProperties {
color: string;
x: number;
y: number;
}
// Abstract class Shape with the clone method and common properties for all shapes
abstract class Shape {
public properties: ShapeProperties;
constructor(properties: ShapeProperties) {
this.properties = properties;
}
abstract clone(): Shape;
}
// Concrete class Rectangle extending Shape
class Rectangle extends Shape {
public width: number;
public height: number;
constructor(properties: ShapeProperties, width: number, height: number) {
super(properties);
this.width = width;
this.height = height;
}
clone(): Shape {
let clonedProperties: ShapeProperties = {
color: this.properties.color,
x: this.properties.x,
y: this.properties.y,
};
return new Rectangle(clonedProperties, this.width, this.height);
}
}
// Concrete class Circle extending Shape
class Circle extends Shape {
public radius: number;
constructor(properties: ShapeProperties, radius: number) {
super(properties);
this.radius = radius;
}
clone(): Shape {
let clonedProperties: ShapeProperties = {
color: this.properties.color,
x: this.properties.x,
y: this.properties.y,
};
return new Circle(clonedProperties, this.radius);
}
}
// Create a red rectangle
let redRectangle: Shape = new Rectangle({ color: "red", x: 0, y: 0 }, 10, 20);
// Clone the red rectangle
let anotherRedRectangle: Shape = redRectangle.clone();
// Change the color of the clone to blue
anotherRedRectangle.properties.color = "blue";
console.log(redRectangle);
// Outputs: Rectangle { properties: { color: 'red', x: 0, y: 0 }, width: 10, height: 20 }
console.log(anotherRedRectangle);
// Outputs: Rectangle { properties: { color: 'blue', x: 0, y: 0 }, width: 10, height: 20 }
Time To Transition From JavaScript To TypeScript
Level Up Your TypeScript And Object Oriented Programming Skills. The only complete TypeScript course on the marketplace you building TypeScript apps like a PRO.
SEE COURSE DETAILSAdvantages of the Prototype Pattern
The Prototype pattern offers several benefits:
Avoid Referrence Errors
In JavaScript and TypeScript, when you assign an object to a new variable, you're actually assigning a reference to the original object, not creating a completely new object. This means if you modify the new object, you're also modifying the original object. This is often a source of bugs and can lead to reference errors.
let original = { name: "John" };
let copy = original;
copy.name = "Jane"; // This also changes 'original'
console.log(original.name); // Outputs 'Jane', not 'John'
The Prototype pattern, when used with deep cloning, can help avoid this problem. Deep cloning means to copy all values of the original object, including nested objects and arrays, into a completely new object.
abstract class Shape {
public properties: ShapeProperties;
constructor(properties: ShapeProperties) {
this.properties = properties;
}
abstract clone(): Shape;
}
class Rectangle extends Shape {
clone(): Shape {
let clonedProperties: ShapeProperties = {
color: this.properties.color,
x: this.properties.x,
y: this.properties.y,
};
return new Rectangle(clonedProperties);
}
}
In this case, clonedProperties
is a new object, not a reference
to this.properties
. Modifying clonedProperties
won't affect the original
object's properties
.
This use of the Prototype pattern can prevent reference errors and unexpected side effects, because you're working with a new object, not modifying the original object indirectly through a reference.
Efficient Object Cloning
If creating a new object involves a heavy database read or computation, cloning an existing object can save these resources.It allows you to clone complex objects without coupling to their concrete classes.
In the Shape example, we can clone any shape regardless of its concrete
class (Rectangle, Circle, etc.). All we need is a reference to an object that
implements the clone
method:
let anotherRedRectangle: Shape = redRectangle.clone();
Here, redRectangle
could be an instance of any class as long as it
implements the Shape
abstract class. This provides a great deal of
flexibility and helps to reduce coupling in your code.
Efficient Adding and Removing Properties at Runtime
You can save resources when the creation of a new object is resource-intensive. In certain cases, creating a new object can be a computationally expensive operation. By cloning an existing object, you can avoid the overhead associated with initializing a new object.
Let's say calculating the area of the shape is a computationally expensive operation which happens during the initialization of the object. Once we have a created shape, we can simply clone it to create a similar object without incurring the computational cost associated with calculating the area again:
// After doing the heavy calculations
let circle: Shape = new Circle({ color: "blue", x: 10, y: 10 }, 20);
// No need to calculate the area again, just clone it
let anotherCircle: Shape = circle.clone();
Simplify Object Creation
It can simplify object creation in systems with complex object relationships or configurations. When objects are composed of several interconnected parts, cloning can help ensure these connections are maintained without having to reconstruct them manually.
For example, let's say that our shapes can have child shapes, making up a more complex shape (like a drawing). If we clone a complex shape, we want all child shapes to be cloned too:
let complexShape: ComplexShape =
new ComplexShape(/* includes multiple shapes */);
let clonedComplexShape: ComplexShape = complexShape.clone();
Here, clonedComplexShape
is a clone of complexShape
, including all its
child shapes. We didn't have to clone each child shape manually – the
Prototype pattern took care of it for us. This makes it much easier to manage
complex, composite objects.
What Can You Do Next 🙏😊
If you liked the article, consider subscribing to Cloudaffle, my YouTube Channel, where I keep posting in-depth tutorials and all edutainment stuff for ssoftware developers.