Constructor Design Pattern
Created: June 30th, 2024
Updated: July 22nd, 2024
Explanation
The Constructor Design Pattern is a creational design pattern and it is automatically invoked every time we instantiate a new instance of a class using the new operator. This method is responsible for initializing the state of the newly created instance by assigning its initial properties and methods. If the class is a subclass, meaning it inherits from a superclass, the constructor invokes the superclassâs constructor to ensure newly created instances are assigned the properties and methods of the superclass.
In class-based, object-oriented programming, a constructor (abbreviation: ctor) is a special type of function called to create an object. It prepares the new object for use, often accepting arguments that the constructor uses to set required member variables.
Analogy
We can think of a bicycle factory as an analogy for the constructor design pattern. A bicycle factory probably uses a blueprint to ensure all bicycles have the same components, such as tires, chains, and a gear shifting mechanism. This is similar to how a constructor method assigns initial properties and methods to new instances. Just as the factory may produce some parts in-house and purchase others, a subclass might initialize some properties and methods and invoke the superclassâs constructor for inheriting properties and methods.
Code Example
ES6 Syntax
class Bicycle {
// Class fields are instance specific properties or methods
model = 'regular bike';
chain = 'standard chain';
year = 2024;
tires = 2;
constructor(color, size) {
// Properties defined in the constructor are instance specific
this.color = color;
this.size = size;
// See 'Wrapping Up' for more on defining methods in the constructor
this.someMethod = function () {
// Do something
};
}
honk() {
console.log('honk');
}
gearShiftingMechanism() {
// Regular bike implementation
}
}
// Instantiate a new instance of Bicycle class
const bicycle = new Bicycle('blue', 'large');
MountainBike
is a subclass of Bicycle
and therefore it needs to invoke the
constructor of its superclass by using the super
keyword and passing in the
required arguments.
class MountainBike extends Bicycle {
model = 'mountain bike';
chain = 'advanced chain';
terrainTypes = ['trail', 'downhill', 'cross-country'];
constructor(color, size, terrainType) {
// super needs to be invoked before using `this` keyword
super(color, size);
if (!this.terrainTypes.includes(terrainType)) {
throw new Error('Invalid terrain type');
} else {
this.terrainType = terrainType;
}
}
// Override the method defined in the superclass by
// Adding a specific implementation for mountain bikes
gearShiftingMechanism() {
// Mountain bike implementation
}
}
// Instantiate a new instance of Bicycle class
const mountainBike = new MountainBike('black', 'medium', 'trail');
Constructors Before ES6
Prior to ES6, the class syntax that we used above did not exist. The keywords class
, extends
, super
, and the constructor
method were not a thing. Instead, constructor functions, which by convention should start with a capital
letter, were used to create objects (instances), and the inheritance chain was setup by assigning the prototype of
the constructor function to an object created from the prototype of another constructor function. This allowed failed lookup
calls on instance methods or properties to be delegated to the prototype of the parent constructor function. To share methods
between all instances of a constructor function, the method had to be defined on the prototype of the constructor function.
Here is an example of the Bicycle and MountainBike classes using constructor functions:
ES5 Syntax
function Bicycle(color, size){
// Equivalent to instance specific properties
this.modal = 'regular bike';
this.chain = 'standard chain';
this.year = 2024;
this.tires = 2;
this.color = color;
this.size = size;
}
Bicycle.prototype.honk = function(){
console.log('honk');
}
Bicycle.prototype.gearShiftingMechanism = function(){}
var bicycle = new Bicycle('blue', 'large');
function MountainBike(color, size, terrainType){
this.model = 'mountain bike';
this.chain = 'advanced chain';
this.terrainTypes = ['trail', 'downhill', 'cross-country'];
// Equivalent to super(color, size);
Bicycle.call(this, color, size)
}
// Setup the inheritance chain
MountainBike.prototype = Object.create(Bicycle.prototype);
// Reset the constructor property
// Resetting the constructor is important for correctly
// identifying the constructor of an instance and for other reasons.
MountainBike.prototype.constructor = MountainBike
// Steps for defining methods on the prototype of MountainBike:
// 1. Assign the prototype of MountainBike to a new object created
// from the Bicycle.prototype object.
// 2. Reset the constructor property to point back to MountainBike.
// 3. Define methods on the prototype of MountainBike.
// Otherwise the methods/properties will be lost.
MountainBike.prototype.gearShiftingMechanism = function(){
// This method will override the method inherited from
// the Bicycle prototype
}
var mountainBike = new MountainBike('black', 'medium', 'trail');
Understanding Accessing Function Scope Variables in Prototype Methods
The #
symbol in ES6 is used to define private
class fields, which are instance-specific properties or methods. These class fields can
only be accessed from within the class where theyâre defined; otherwise, an error will be thrown if they are accessed from outside the
class.
In both ES5 and ES6, to achieve data privacy in any type of function, such as a constructor function, we can define the property using var
,
const
or let
instead of on the this
keyword and access it through the use
of closures . However, this solution leads to the
problem where methods defined on the prototype of the constructor function cannot access that private property
. Itâs easier to show this
behavior with a code example:
ES6 Syntax
class Bicycle {
#serialNumber = 123;
constructor(color, size) {
this.color = color;
this.size = size;
}
logSerialNumber() { console.log(this.#serialNumber); }
}
const bicycle = new Bicycle('blue', 'large');
console.log(bicycle.#serialNumber); // output: SyntaxError //
bicycle.logSerialNumber(); // output: 123 //
Everything works as expected; the private class field #serialNumber
is not accessible from outside the class, but the method logSerialNumber
is able to access the private class field.
ES5 Syntax
function Bicycle(color, size) {
var serialNumber = 123;
this.color = color;
this.size = size;
}
Bicycle.prototype.logSerialNumber = function () {
// will throw 'ReferenceError: serialNumber is not defined'
console.log(serialNumber);
};
var bicycle = new Bicycle('blue', 'large');
console.log(bicycle.serialNumber); // output: undefined
bicycle.logSerialNumber(); // output: the above error
So, since we want to keep serialNumber
private, weâll define it using const
instead of on the this
keyword because the âthisâ
keyword represents the instance returned by the constructor function, and therefore, if it was defined on the instance, it can be accessed
by doing this.serialNumber
and so it wouldnât be private. But this privacy means that methods defined on the prototype of
the constructor function wonât be able to access âserialNumberâ as it is defined within the constructor functionâs scope
and not on the
instance itself.
However, methods and functions defined within the scope
of the constructor function can access âserialNumberâ because they were either
defined in the same top level scope or have access due to closures.
function Bicycle(color, size) {
const serialNumber = 123;
this.color = color;
this.size = size;
this.logSerialNumber = function () { log(); };
function log() { console.log(serialNumber); }
}
var bicycle = new Bicycle('blue', 'large');
console.log(bicycle.serialNumber); // output: undefined
bicycle.logSerialNumber(); // output: 123
So, this leads to the realization that there is a trade-off to be made between memory efficiency
and access to private data
.
If the objective is to minimize memory usage
, then methods should be defined on the prototype of the constructor function, as these methods are shared between all instances. However, these methods will not have access to properties or functions that are defined within the scope of the constructor function, as they are considered âprivateâ to the constructor function.If the objective is to have access to private data
, then we should define methods within the constructor function. These methods will have access to variables or functions defined within the constructor function because they were either defined in the same top level scope or have access due to closures. However, this results in using more memory as each instance will have its own copy of these methods.
Wrapping Up
Some sources do not view the constructor as a design pattern but rather as a method of a class for initializing properties and methods of a new instance. Also, many definitions, just like mine, say that it is responsible for initializing properties and methods. But I have not come across a use case for initializing methods within the constructor, and if we were to initialize a method within the constructor, it would be unique for each instance and therefore not a method in the sense of what a method means in OOP: a function on a class that is shared between all instances of that class.
But, as my mentor Chase pointed out, MDN says,
A property's value can be a function, in which case the property is known as a method.
He also pointed out that we developers
only write the constructor method, but behind the scenes, there is probably a lot more going on in the construction phase. So,
therefore, there probably is a use case for initializing methods within the constructor that I just donât know about due to my
lack of knowledge.
Iâve also asked Jonny Magrippis , who is a great instructor, and even he says he hasnât seen a legit use case for it. If you do know of a use case, please share!