Factory Design Pattern
Created: July 20th, 2024
Updated: July 22nd, 2024
Explanation
The Factory Design Pattern is a creational design pattern that encapsulates logic for instantiating instances of various classes. However, the pattern does not encapsulate the logic for initializing the state and/or behavior of those instances; those implementation details are delegated to the classes of the instances themselves. Therefore, the pattern doesn’t know the inner workings of the classes it instantiates instances of; it’s decoupled from those classes.
In object-oriented programming, a factory is an object for creating other objects; formally, it is a function or method that returns objects of a varying prototype or class from some method call, …
Analogy
We can think of a logistics broker as the factory in the Factory Design Pattern. This broker might provide services to move items from point A to point B using a variety of different types of transportation, such as land, air, sea, rail, etc. The broker delegates the transportation requests to various logistic companies. These logistic companies can be thought of as the subclasses in the pattern, each handling the actual implementation details of a type of transportation.
Code Example
ES6 Syntax
class LogisticsBroker {
createTransportationService(request) {
switch (request.type) {
case 'land':
return new LandTransportationService(request);
case 'air':
return new AirTransportationService(request);
case 'sea':
return new SeaTransportationService(request);
case 'rail':
return new RailTransportationService(request);
default:
throw new Error('Invalid transportation type');
}
}
}
class LandTransportationService {
constructor(request) {
this.request = request;
}
calculateCost(from, to) { /* implementation */ }
schedueTransportation(from, to) { /* implementation */ }
trackTransportationStatus(){ /* implementation */ }
}
class AirTransportationService { /* implementation */ }
class SeaTransportationService { /* implementation */ }
class RailTransportationService { /* implementation */ }
// usage
const request = {
type: 'land',
fromAddress: '123 Main St Ottawa, ON',
toAddress: '123 Main St Toronto, ON',
date: '2024-07-20',
items: [ /* ... */]
};
const broker = new LogisticsBroker();
const service = broker.createTransportationService(request);
The advantages of the factory design pattern can be seen here. The LogisticsBroker class simplifies the client’s interaction
by abstracting away all the details and making broker.createTransportationService(request)
method the only interface available
to request a transportation service.
This abstraction also provides the logisticsBroker class flexibility. Its implementation details or the types of transportation services it provides can be added to or removed, but as long as it provides the broker.createTransportationService(request) method as an interface, the client’s usage of the class won’t be impacted.
Also, since the implementation details of the other transportation services classes are encapsulated within their respective classes and therefore abstracted away from the logisticsBroker, this decoupling simplifies the overall code, which makes it easier to maintain and modify different sections without impacting the rest of the code.
Factory Pattern Before ES6
Prior to ES6, the class syntax that we used above did not exist, so factory functions were used instead, which are regular functions that return an object. The code example below re-implements the logisticsBroker class, but using ES5 syntax.
ES5 Syntax
function logisticsBroker() {
return {
createTransportationService: function (request) {
switch (request.type) {
case 'land':
return landTransportationService(request);
case 'air':
return airTransportationService(request);
case 'sea':
return seaTransportationService(request);
case 'rail':
return railTransportationService(request);
default:
throw Error('Invalid transportation type');
}
}
};
};
function landTransportationService(request) {
return {
calculateCost: function (from, to) {},
schedueTransportation: function (from, to) {},
trackTransportationStatus: function () {},
};
}
function airTransportationService(request) { return {}; }
function seaTransportationService(request) { return {}; }
function railTransportationService(request) { return {}; }
// usage
var request = {
type: 'land',
fromAddress: '123 Main St Ottawa, ON',
toAddress: '123 Main St Toronto, ON',
date: '2024-07-20',
items: [ /* */]
};
var broker = logisticsBroker();
var service = broker.createTransportationService(request);
However, this implementation isn’t memory efficient as the methods get defined for each object created from the factory function.
At first, I thought we could just define the methods on the prototype of the factory function, but that doesn’t work because the
objects returned from the factory function are not instances of the factory function and therefore don’t inherit from the prototype
of the factory function. However, there is a solution, we can define an object that contains all the methods for each factory function,
and then, within each factory function, we can create an object using Object.create(proto)
and pass the object containing the methods
as the proto
argument. This will set up the prototypal inheritance chain for the newly created object by setting its internal
[[Prototype]] property to the proto
argument.
Here’s the code example:
ES5 Syntax
var logisticsBrokerMethods = {
createTransportationService: function (request) {
switch (request.type) {
case 'land':
return landTransportationService(request);
case 'air':
return airTransportationService(request);
case 'sea':
return seaTransportationService(request);
case 'rail':
return railTransportationService(request);
default:
throw Error('Invalid transportation type');
}
}
};
var landTransportationServiceMethods = {
calculateCost: function (from, to) { /* implementation */ },
scheduleTransportation: function (from, to) { /* implementation */ },
trackTransportationStatus: function () { /* implementation */ },
};
var airTransportationServiceMethods = { /* implementation */ };
var seaTransportationServiceMethods = { /* implementation */ };
var railTransportationServiceMethods = { /* implementation */ };
function logisticsBroker() {
return Object.create(logisticsBrokerMethods);
};
function landTransportationService(request) {
var service = Object.create(landTransportationServiceMethods);
service.request = request;
return service;
}
function airTransportationService(request) {
var service = Object.create(airTransportationServiceMethods);
service.request = request;
return service;
}
function seaTransportationService(request) {
var service = Object.create(seaTransportationServiceMethods);
service.request = request;
return service;
}
function railTransportationService(request) {
// we can also use a data descriptor to define the request property
return Object.create(railTransportationServiceMethods, {
request: {
value: request,
writable: true,
enumerable: true,
configurable: true
}
});
};
// usage
// same as before
This implementation is memory efficient, but it brings about a behavior that is important to be aware of:
any property or function defined within a factory function's scope is not accessible by methods defined on the prototype
of the service
object. For example, the newly created service
object within the landTransportationService
factory function inherits all the methods
from the proto
object passed to the Object.create(proto)
. All those inherited methods do not have access to any property or function
defined witin the scope of the landTransportationService
factory function, because those properties or functions become “private” to that
factory function. However, methods and functions defined within the factory function scope can access those properties because they were
either defined in the same top level scope or have access due to closures.
I’ve included a section on this behavior in my Constructor Design Pattern post, please check out the section titled Understanding Accessing Function Scope Variables in Prototype Methods .
Whether we use this variation or the previous one, factory functions grant us the ability to have a private interface where we can define private properties or private functions. If you’re interested, I’ve covered private and public interfaces, which come about due to closures, in my Module Design Pattern article .
Wrapping Up
At the beginning of this article, I was a little confused as to what really differentiated the factory design pattern from
the constructor design pattern , because the ES5 example above
can be implemented using the constructor pattern, but at that point, it can’t be considered a factory pattern. Therefore, I’ve
learned that the moment we use the new
operator, the ES5 example, with slight modifications, is considered a constructor pattern.
Through that insight, it now makes sense to me why the instanceof
operator returns false
for objects created from factory functions,
because factory functions return objects, not instances. Instances are only created when we use the new
operator with a constructor
function or class. This explains why those objects inherit from Object.prototype, unless the prototype has been explitcly set to another
object, and not from the factory function’s prototype object.
The comparison has also highlighted the simplicity of creating closures using factory functions as an advantage over constructor functions.
Although, It’s not too difficult with constructor functions either, but it’s not as clean as the factory function. But my main insight has
been that for both design patterns, there is a trade-off to be made between memory efficiency
and access to private data
.
I really like the factory design pattern!
I hope you’ve found this article helpful. Thank you for reading!