Extensible Systems
Very often we want to support some kind of plug-in or add-on mechanism in our apps but end up with code which is full of magic numbers, functions that end up knowing about the nuances of every add-on, etc. This makes the code a monolith and this makes updating and restructuring a nightmare.
Ideally what we’d like in our code is:
- An app that knows how to use generic add-ons but doesn’t know anything about specific ones
- A set of add-on implementations in code that might be in various libraries or other locations
The answer for how to achieve this is the Factory Pattern.
The Factory Pattern
Put simply, the factory pattern allows you to separate the creation of an object from the code which uses the object. To implement the pattern we need several things:
- a generic function or interface that can create an object of the right base type
- a map of add-on keys or names that reference “creator” functions (of the generic form above)
- an object – often a singleton or owned at a high level in the app which performs the “factory” function
The key point here is that we map a key (which might be the name of the add-on or some reference number) to a function that is used to create an object that implements the add-on functionality.
Concrete Code (in TypeScript)
Here’s a simple factory example:
interface Dictionary<T> {
[key: string]: T;
}
export abstract class AddOnBase {
}
type AddOnCreator = (name:string) => AddOnBase;
class FactoryRecord {
creator!: AddOnCreator;
}
class AddOnFactory {
_addOnMap: Dictionary<FactoryRecord> = {};
register(type: string, addOnCreator: AddOnCreator) {
this._addOnMap[type] = { "creator": addOnCreator };
}
create(type: string, name: string): AddOnBase | null {
if (type in this._addOnMap) {
return this._addOnMap[type].creator(name);
}
return null;
}
}
const addOnFactory = new AddOnFactory();
export default addOnFactory;
The AddOnFactory has two functions:
register() is used to store the creator function so that it can be looked up later. The register() function is generally called by code in the add-on library. Multiple add-on libraries might be managed in this way and each is asked to register all of their add-ons with the factory at initialisation time.
create() is used to generate add-on objects as required. For instance, in an app with a user interface the user might be offered a list of add-ons – generated from the “type” that is used as the key to the add-on map or from additional information that might be registered along with the creator function.
As long as the registration and creation process are generic then any amount of information can be registered and the creator function can take some or all of this information as parameters when it is called.
A Complete Example
A full example is available on GitHub
Poor Tutorials
Unfortunately I think a lot of tutorials on the Factory Pattern miss the point about abstraction. They end up with a function inside the factory that has a switch statement to construct each of the concrete types. This, of course, doesn’t avoid the hard link between the factory (and app) code and the specific add-on implementations.