Skip to main content

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 }
TypeScript Course Instructor Image
TypeScript Course Instructor Image

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 DETAILS

Advantages 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.

YouTube @cloudaffle