Skip to content

Prototype Design Pattern

Video Lecture

Section Video Links
Prototype Pattern Prototype Prototype Pattern  
Prototype Use Case Prototype Use Case Prototype Use Case  

Overview

The Prototype design pattern is good for when creating new objects requires more resources than you want to use or have available. You can save resources by just creating a copy of any existing object that is already in memory.

E.g., A file you've downloaded from a server may be large, but since it is already in memory, you could just clone it, and work on the new copy independently of the original.

In the Prototype patterns interface, you create a clone method that should be implemented by all classes that use the interface. How the clone method is implemented in the concrete class is up to you. You will need to decide whether a shallow or deep copy is required.

  • A shallow copy, copies and creates new references one level deep,
  • A deep copy, copies and creates new references for all levels.

In JavaScript, you have mutable objects such as Arrays, Dictionaries, Sets and any custom Objects you may have created. A shallow copy, will create new copies of the objects with new references in memory, but the underlying data, e.g., the actual elements in an array, will point to the same memory location as the original array/object being copied. You will now have two arrays, but the elements within the arrays will point to the same memory location. So, changing any elements of a copied array will also affect the original array. Be sure to test your implementation that the copy method you use works as expected. Shallow copies are much faster to process than deep copies and deep copies are not always necessary if you are not going to benefit from using it.

Terminology

  • Prototype Interface: The interface that describes the clone() method.
  • Prototype: The Object/Product that implements the Prototype interface.
  • Client: The client application that uses and creates the ProtoType.

Prototype UML Diagram

Prototype UML Diagram

Source Code

Experiment with the concept code.

By default, it will shallow copy the object you've asked to be cloned.

In my example, I have created an array of numbers. At first impressions, when this array is copied, it will appear that the array was fully cloned. But the inner items of the array were not. They will point to the same memory location as the original array; however, the memory identifier of the new array is new and different from the original.

In the MyClass.clone() method, there is a line return JSON.parse(JSON.stringify(this)); that is commented out. Uncomment out this line, and comment out the line before it to now be // return Object.assign({}, this). Re compile and execute the file, and now the array items will be copied as well. This is a deep copy.

Remember that full deep copies can potentially be much slower for very complicated object hierarchies.

./src/prototype/prototype-concept.ts

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
interface IProtoType {
    // interface with clone method
    clone(): this
    // The clone is deep or shallow.
    // It is up to you how you want to implement
    // the details in your concrete class
}

class MyClass implements IProtoType {
    // A Concrete Class
    field: number[]

    constructor(field: number[]) {
        this.field = field // any value of any type
    }

    clone() {
        return Object.assign({}, this) // shallow copy
        // return JSON.parse(JSON.stringify(this)); //deep copy
    }
}

// The Client
// Create an object containing an array
const OBJECT1 = new MyClass([1, 2, 3, 4])
console.log(`OBJECT1: ${JSON.stringify(OBJECT1)}`)

const OBJECT2 = OBJECT1.clone() // Clone
console.log(`OBJECT2: ${JSON.stringify(OBJECT2)}`)
// Change the value of one of the array elements in OBJECT2
// Depending on your clone method, either a shallow or deep copy
// was performed
OBJECT2.field[1] = 101

// Comparing OBJECT1 and OBJECT2
console.log(`OBJECT2: ${JSON.stringify(OBJECT2)}`)
console.log(`OBJECT1: ${JSON.stringify(OBJECT1)}`)

Output

When using the shallow copy approach. Changing the inner item of OBJECT2s array, also affected OBJECT1s array.

node ./dist/prototype/prototype-concept.js
OBJECT1: {"field":[1,2,3,4]}
OBJECT2: {"field":[1,2,3,4]}
OBJECT2: {"field":[1,101,3,4]}
OBJECT1: {"field":[1,101,3,4]}

When using the deep copy approach. Changing the inner item of OBJECT2s array, does not affect OBJECT1s array.

node ./dist/prototype/prototype-concept.js
OBJECT1: {"field":[1,2,3,4]}
OBJECT2: {"field":[1,2,3,4]}
OBJECT2: {"field":[1,101,3,4]}
OBJECT1: {"field":[1,2,3,4]}

Prototype Use Case

In this example, an object called document is cloned using shallow and deep methods.

I clone the documents instance properties and methods.

The object contains an array of two arrays. Three copies are created, and each time some part of the array is changed on the clone, and depending on the method used, it can affect the original object.

When cloning an object, it is good to understand the deep versus shallow concept of copying and whether you also want the clone to contain the classes methods.

Example UML Diagram

Prototype Use Case Diagram

Source Code

./src/prototype/client.ts

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// Prototype Use Case Example Code
import Document from './document'

// Creating a document containing an array of two arrays
const ORIGINAL_DOCUMENT = new Document('Original', [
    [1, 2, 3, 4],
    [5, 6, 7, 8],
])
console.log(ORIGINAL_DOCUMENT)
console.log()

const DOCUMENT_COPY_1 = ORIGINAL_DOCUMENT.clone(1) // shallow copy
DOCUMENT_COPY_1.name = 'Copy 1'
// This also modified ORIGINAL_DOCUMENT because of the shallow copy
// when using mode 1
DOCUMENT_COPY_1.array[1][2] = 200
console.log(DOCUMENT_COPY_1)
console.log(ORIGINAL_DOCUMENT)
console.log()

const DOCUMENT_COPY_2 = ORIGINAL_DOCUMENT.clone(1) // shallow copy
DOCUMENT_COPY_2.name = 'Copy 2'
// This does NOT modify ORIGINAL_DOCUMENT because it changes the
// complete array[1] reference that was shallow copied when using mode 1
DOCUMENT_COPY_2.array[1] = [9, 10, 11, 12]
console.log(DOCUMENT_COPY_2)
console.log(ORIGINAL_DOCUMENT)
console.log()

const DOCUMENT_COPY_3 = ORIGINAL_DOCUMENT.clone(2) // deep copy
DOCUMENT_COPY_3.name = 'Copy 3'
// This does modify ORIGINAL_DOCUMENT because it changes the element of
// array[1][0] that was deep copied recursively when using mode 2
DOCUMENT_COPY_3.array[1][0] = 1234
console.log(DOCUMENT_COPY_3)
console.log(ORIGINAL_DOCUMENT)
console.log()

./src/prototype/document.ts

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// A sample document to be used in the Prototype example
import ProtoType from './iprototype'

export default class Document implements ProtoType {
    name: string
    array: [number[], number[]]

    constructor(name: string, array: [number[], number[]]) {
        this.name = name
        this.array = array
    }

    clone(mode: number): Document {
        // This clone method uses different copy techniques
        let array
        if (mode === 2) {
            // results in a deep copy of the Document
            array = JSON.parse(JSON.stringify(this.array))
        } else {
            // default, results in a shallow copy of the Document
            array = Object.assign([], this.array)
        }
        return new Document(this.name, array)
    }
}

./src/prototype/iprototype.ts

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// Prototype Concept Sample Code

import Document from './document'

export default interface IProtoType {
    // interface with clone method
    clone(mode: number): Document
    // The clone, deep or shallow.
    // It is up to you how you  want to implement
    // the details in your concrete class"""
}

Output

node ./dist/prototype/client.js
Document {
  name: 'Original',
  array: [ [ 1, 2, 3, 4 ], [ 5, 6, 7, 8 ] ]
}

Document {
  name: 'Copy 1',
  array: [ [ 1, 2, 3, 4 ], [ 5, 6, 200, 8 ] ]
}
Document {
  name: 'Original',
  array: [ [ 1, 2, 3, 4 ], [ 5, 6, 200, 8 ] ]
}

Document {
  name: 'Copy 2',
  array: [ [ 1, 2, 3, 4 ], [ 9, 10, 11, 12 ] ]
}
Document {
  name: 'Original',
  array: [ [ 1, 2, 3, 4 ], [ 5, 6, 200, 8 ] ]
}

Document {
  name: 'Copy 3',
  array: [ [ 1, 2, 3, 4 ], [ 1234, 6, 200, 8 ] ]
}
Document {
  name: 'Original',
  array: [ [ 1, 2, 3, 4 ], [ 5, 6, 200, 8 ] ]
}

Summary

  • Just like the other creational patterns, a Prototype is used to create an object at runtime.
  • A Prototype is created from an object that is already instantiated. Imagine using the existing object as the class template to create a new object, rather than calling a specific class. Note that, the clone method used in the concept video demonstrated didn't copy the class methods to the new object. The clones only contained copies of the instance properties. If you want your new clone to have the same methods of the original class, then use the classes' constructor when returning the clone as I did in the clone(mode) method in document.ts.
  • The ability to create a Prototype means that you don't need to create many classes for specific combinations of objects. You can create one object, that has a specific configuration, then clone it and alter some factor of it, then create another clone from this altered configuration, and keep continuing to create many objects which are all slightly different from each other.
  • New Prototypes can be created at runtime, without knowing what kind of attributes the prototype may eventually have. E.g., You have a sophisticated object that was randomly created from many factors, and you want to clone it rather than adding all those same factors over and over again until the new object matches the one that could have just been cloned.
  • A prototype is also useful for when you want to create a copy of an object, but creating that copy may be very resource intensive. E.g., you can either create a new houseboat from the builder example, or clone an existing houseboat from one already in memory.
  • When designing your clone() method, you should consider which elements will be shallow copied or deep copied.