Creational Design Patterns
Manoj Kumar Patra
Posted on October 2, 2024
# Factory
Ability to decouple the creation of an object from one particular implementation.
Inside the factory, we can choose to create a new instance of a class using the new operator, or leverage closures to dynamically build a stateful object literal, or even return a different object type based on a particular condition.
A factory can also be used as an encapsulation mechanism, thanks to closures.
⭐ Encapsulation refers to controlling the access to some internal details of a component by preventing external code from manipulating them directly.
EXAMPLE 1
function createPerson (name) {
const privateProperties = {};
const person = {
setName (name) {
if (!name) {
throw new Error('A person must have a name');
}
privateProperties.name = name;
}
getName () {
return privateProperties.name;
}
};
person.setName(name);
return person;
}
Different ways to enforce encapsulation:
- Using closures
- 🔗 Using private class fields (the hashbang # prefix syntax)
- 🔗 Using WeakMaps
- 🔗 Using symbols
- 🔗 Defining private variables in a constructor
EXAMPLE 2
class Profiler {
constructor(label) {
this.label = label;
this.lastTime = null;
}
start() {
/**
* process.hrtime() is used to measure performance
* in a more precise manner than Date.now(),
* as it provides timestamps in nanoseconds.
*
* The returned value is an array with two elements:
* The first element is the number of seconds.
* The second element is the number of nanoseconds.
*/
this.lastTime = process.hrtime();
}
end() {
const diff = process.hrtime(this.lastTime);
console.log(`Timer "${this.label}" took ${diff[0]} seconds ` +
`and ${diff[1]} nanoseconds`);
}
}
const noopProfiler = {
start() {},
end() {},
};
export function createProfiler(label) {
if (process.env.NODE_ENV === 'production') {
return noopProfiler;
}
return new Profiler(label);
}
Examples of npm libraries using factory pattern - knex
# Builder
Builder is a creational design pattern that simplifies the creation of complex objects by providing a fluent interface, which allows us to build the object step by step.
EXAMPLE 1
class Boat {
constructor(hasMotor, motorCount, motorBrand,
motorModel, hasSails, sailsCount,
sailsMaterial, sailsColor, hullColor,
hasCabin) {}
// ...
}
class BoatBuilder {
withMotors(count, brand, model) {
this.hasMotor = true;
this.motorCount = count;
this.motorBrand = brand;
this.motorModel = model;
return this;
}
withSails(count, material, color) {
this.hasSails = true;
this.sailsCount = count;
this.sailsMaterial = material;
this.sailsColor = color;
return this;
}
hullColor(color) {
this.hullColor = color;
return this;
}
withCabin() {
this.hasCabin = true;
return this;
}
build() {
return new Boat({
hasMotor: true,
motorCount: 2,
motorBrand: 'Best Motor Co.',
motorModel: 'OM123',
hasSails: true,
sailsCount: 1,
sailsMaterial: 'fabric',
sailsColor: 'white',
hullColor: 'blue',
hasCabin: false
});
}
}
const myBoat = new BoatBuilder()
.withMotors(2, 'Best Motor Co.', 'OM123')
.withSails(1, 'fabric', 'white')
.withCabin()
.hullColor('blue')
.build();
⭐ Using a builder that is separate from the target class has the advantage of always producing instances that are guaranteed to be in a consistent state.
EXAMPLE 2
export class Url {
constructor (protocol, username, password, hostname,
port, pathname, search, hash) {
this.protocol = protocol;
this.username = username;
this.password = password;
this.hostname = hostname;
this.port = port;
this.pathname = pathname;
this.search = search;
this.hash = hash;
this.validate();
}
validate() {
if (!this.protocol || !this.hostname) {
throw new Error(`Must specify at least a protocol and a hostname`);
}
}
toString() {
let url = '';
url += `${this.protocol}://`;
if (this.username && this.password) {
url += `${this.username}:${this.password}`;
}
url += this.hostname;
if (this.port) {
url += this.port;
}
if (this.pathname) {
url += this.pathname;
}
if (this.search) {
url += `?${this.search}`;
}
if (this.hash) {
url += `#${this.hash}`;
}
return url;
}
}
export class UrlBuilder {
setProtocol(protocol) {
this.protocol = protocol;
return this;
}
setAuthentication(username, password) {
this.username = username;
this.password = password;
return this;
}
setHostname(hostname) {
this.hostname = hostname;
return this;
}
setPort(port) {
this.port = port;
return this;
}
setPathname(pathname) {
this.pathname = pathname;
return this;
}
setSearch(search) {
this.search = search;
return this;
}
setHash(hash) {
this.hash = hash;
return this;
}
build() {
return new Url(this.protocol, this.username, this.password,
this.hostname, this.port, this.pathname, this.search,
this.hash);
}
}
const url = new UrlBuilder()
.setProtocol('https')
.setAuthentication('user', 'pass')
.setHostname('example.com')
.build();
# Revealing constructor
Useful when we want to allow an object's internals to be manipulated only during its creation phase.
Use cases
- Creating objects that can be modified only at creation time
- Creating objects whose custom behavior can be defined only at creation time
- Creating objects that can be initialized only once at creation time
Syntax
// (1) (2) (3)
const object = new SomeClass(function executor(revealedMembers) { // manipulation code ...
})
Immutable objects
Immutable refers to the property of an object by which its data or state becomes unmodifiable once it's been created.
- Modifying an immutable object can only be done by creating a new copy and can make the code more maintainable and easier to reason about.
- Efficient change detection with strict equality operator
Immutable version of the Node.js Buffer component
const MODIFIER_NAMES = ["swap", "write", "fill"];
export class ImmutableBuffer {
constructor(size, executor) {
const buffer = Buffer.alloc(size);
const modifiers = {};
for (const prop in buffer) {
if (typeof buffer[prop] !== "function") {
continue;
}
if (MODIFIER_NAMES.some(modifier => prop.startsWith(modifier))) {
modifiers[prop] = buffer[prop].bind(buffer);
} else {
this[prop] = buffer[prop].bind(buffer);
}
}
executor(modifiers);
}
}
// USAGE
const hello = "Hello!";
const immutable = new ImmutableBuffer(hello.length, ({ write }) => {
write(hello);
});
console.log(String.fromCharCode(immutable.readInt8(0)));
A popular application of the Revealing Constructor pattern is in the JavaScript Promise class.
# Singleton
Reasons to use a single instance across all the components of an application:
- For sharing stateful information
- For optimizing resource usage
- To synchronize access to a resource
The module is cached using its full path as the lookup key, so it is only guaranteed to be a singleton within the current package.
❓ What happens when package-a
and package-b
have a dependency on the mydb
package, but package-a
depends on version 1.0.0
of the mydb
package, while package-b
depends on version 2.0.0
of the same package?
app/
`-- node_modules
|-- package-a
| `-- node_modules
| `-- mydb
`-- package-b
`-- node_modules
`-- mydb
In such a case, a typical package manager such as npm
or yarn
would not "hoist" the dependency to the top node_modules
directory, but it will instead install a private copy of each package in an attempt to fix the version incompatibility.
With the directory structure we just saw, both package-a
and package-b
have a dependency on the mydb
package; in turn, the app package, which is our root package, depends on both package-a
and package-b
.
import { getDbInstance as getDbFromA } from 'package-a';
import { getDbInstance as getDbFromB } from 'package-b';
const isSame = getDbFromA() === getDbFromB();
console.log('Is the db instance in package-a the same ' +
`as package-b? ${isSame ? 'YES' : 'NO'}`); // NO
If instead, both package-a
and package-b
required two versions of the mydb
package compatible with each other, for example, ^2.0.1
and ^2.0.7
, then the package manager would install the mydb
package into the top-level node_modules
directory (a practice known as dependency hoisting), effectively sharing the same instance with package-a
, package-b
, and the root package.
# Wiring Modules using dependency injection
The Node.js module system and the Singleton pattern can serve as great tools for organizing and wiring together the components of an application. However, they might introduce a tighter coupling between components.
📖 Dependency Injection (DI) is a very simple pattern in which the dependencies of a component are provided as input by an external entity, often referred to as the injector.
The injector has the goal of providing an instance that fulfills the dependency for the service.
Disadvantage of DI
Dependency Injection forces us to build the dependency graph of the entire application by hand, making sure that we do it in the right order. This can become unmanageable when the number of modules to wire becomes too high.
Alternative - Inversion of Control
📖 Inversion of Control, allows us to shift the responsibility of wiring the modules of an application to a third-party entity. This entity can be a service locator (a simple component used to retrieve a dependency, for example, serviceLocator.get('db')
) or a dependency injection container (a system that injects the dependencies into a component based on some metadata specified in the code itself or in a configuration file).
Posted on October 2, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.