laptop-man

Creating Factory Layouts

In a previous discussion, we introduced the Custom Type Module Pattern and its utility in crafting bespoke objects within your application. While these custom types hold significance in JavaScript development, directly crafting them can swiftly lead to clutter. In this piece, I’ll introduce a novel approach, the Factory pattern, which significantly streamlines the process of working with custom types.

So, what exactly is a Factory?

To grasp the essence of a factory, it’s crucial to understand the problem it aims to address. Directly creating a new custom object necessitates invoking the constructor with the ‘new’ keyword and any requisite arguments. This grants absolute control over creation, a powerful capability indeed, yet not always pragmatic.

Let’s delve into the illustration using solely Custom Types:

var Widget = require('./lib/widget');
// Somewhere in your application…
var redWidget = new Widget(42, true);
redWidget.paintPartA('red');
redWidget.paintPartB('red');
redWidget.paintPartC('red');
// Elsewhere in your application…
var blueWidget = new Widget(42, true);
blueWidget.paintPartA('blue');
blueWidget.paintPartB('blue');
blueWidget.paintPartC('blue');
// And yet again somewhere else…
var greenWidget = new Widget(42, true);
greenWidget.paintPartA('green');
greenWidget.paintPartB('green');
greenWidget.paintPartC('green');

Handling a complex constructor can be excessive in simpler cases or when repeatedly passing the same arguments to constructors becomes a norm. Both scenarios are, at best, inconvenient and, at worst, entirely unscalable. Copypasta in code is never a pleasant sight.

Enter the factory to the rescue. A factory’s role is to create objects so that you don’t have to, offering several advantages. Whether you’re developing a public module for npm or internally within your team, integrating a factory interface can enhance the usability of your module. This is particularly true if additional steps are required post-constructor invocation. A factory consolidates and standardizes the custom creation process for both developers and users, outlining precisely how new objects should be crafted.

With the aid of a factory, the above example transforms into:

var widgetFactory = require('./lib/widget-factory');
var redWidget = widgetFactory.getRedWidget();
var blueWidget = widgetFactory.getBlueWidget();
var greenWidget = widgetFactory.getGreenWidget();

If you encounter struggles with intricate object creation spread across various parts of your application, this pattern could be the solution. By centralizing this logic in a singular location, you prevent its proliferation throughout your codebase where it doesn’t belong.

Separating the Product

Before constructing a factory, ensure that any custom types you intend to utilize are already defined as separate modules. While it might be tempting to house both the factory and the product (the object returned by a factory) within the same file, adhering to the principle of small, single-purpose modules is crucial. Segregating these modules diminishes confusion regarding their distinct responsibilities. In practice, this separation enhances testing coverage and code reusability.

The Factory Design Pattern

The Factory pattern can be built upon either singleton or custom type patterns. If your entire application can share a single factory object, constructing it as a singleton is often the ideal choice. A singleton can be effortlessly required anywhere and necessitates no intricate construction or distribution. However, it’s imperative to remember that your state is confined to the single module.

var Widget = require('./widget');
module.exports = {
getRedWidget: function getRedWidget() {
var widget = new Widget(42, true);
widget.paintPartA('red');
widget.paintPartB('red');
widget.paintPartC('red');
return widget;
},
getBlueWidget: function getBlueWidget() {
// …
}
}

Nonetheless, employing the same factory across your application may not always suffice. Occasionally, a factory demands its setup and customization. In such scenarios, crafting your factory utilizing the custom type pattern is more appropriate. This approach enables your factory to offer a basic level of flexibility with each instance while encapsulating product customization.

var Widget = require('./lib/widget');
var WidgetFactory = module.exports = function WidgetFactory(options) {
this.cogs = options.cogs;
this.bool = options.bool;
}
WidgetFactory.prototype.getRedWidget = function getRedWidget() {
var widget = new Widget(this.cogs, this.bool);
widget.paintPartA('red');
widget.paintPartB('red');
widget.paintPartC('red');
return widget;
};
WidgetFactory.prototype.getBlueWidget = function getBlueWidget() {
// …
};

Abstract Factories

Once familiar with factories, you can explore innovative ways to leverage them. The Abstract Factory pattern, for instance, elevates flexibility by taking the concept a step further. If you possess multiple factories, each crafting different products from a common parent type, establishing a shared interface among them is beneficial. This facilitates seamless interchangeability between factories without necessitating modifications across your codebase.

function someMiddleware(req, res, next) {
if(/* user is logged in */) {
req.userFactory = new LoggedInUserFactory(req.session.userID);
} else {
req.userFactory = new LoggedOutUserFactory();
}
next(); // This will call nextMiddleware() with the same req/res objects
}
function nextMiddleware(req, res, next) {
var user = req.userFactory.getUser();
var credentials = req.userFactory.getCredentials();
res.send('Welcome back, ' + user.fullName);
}

Here, you have two distinct factories for logged-in and logged-out users. They lack a shared interface (createUser() vs. createAnonymousUser()), necessitating constant checks to determine the type of factory in use. This results in unnecessary checks throughout the application, when ideally, only one is needed.

By consolidating each interface into a common framework, you implement an abstract factory. Subsequently, each factory can be utilized without concern for the type of user being created, streamlining decision-making within your application.

Conclusion

A robust custom type interface should possess the flexibility to accommodate all reasonable use cases. However, this flexibility may come at a price. As the array of supported options expands, so does the interface and requisite setup, potentially introducing complexity. Moreover, creation intricacies may permeate your application, leading to duplicated code blocks for each new object. Factories offer a clean solution, restoring order to your application by simplifying and streamlining complex and repetitive customizations, making them more maintainable in the process.