This post is a follow up to the post on Object Composition and Prototypical Inheritance in JavaScript.
If you have decided to write your JavaScript driven application using factories to create objects through composition instead of traditional inheritance then you are likely to end up with a lot of factory code that only changed in the details of the object that it creates.
But writing out a factory function each time you need to provide the ability to create a new object with slightly different attributes means writing redundant code and increasing the script size that the user has to download.
A better way to approach factories is to write a factory factory; that is, a factory function that generates factories for you. If you do this, then you never have to rewrite the factory code again since the factory factory will generate all of your factories for you.
Since factories just take prototype objects which it bases new objects on, all we have to do is return a function from a createFactory()
function which will take an object prototype as it's only argument.
The prototype only needs to be an object that contains state
, methods
and/or closures
.
For example:
// A factory function that will generate factories for us
function createFactory(proto) {
// Return a function that will generate objects based on the given prototype
return function() {
// Create a new object with the common methods
return Object.create(proto.methods || {});
};
}
// Generate a chicken pie factory
var createChickenPie = createFactory({
// Attach some delegate methods to the methods key
methods: {
listIngredients: function() {
console.log('Chicken, Pastry, Sauce');
},
divideThePie: function(slices) {
console.log((Math.PI * 2) / slices);
}
}
});
// Manufacture some chicken pies
var pie1 = createChickenPie();
var pie2 = createChickenPie();
// Call one of the methods
pie1.listIngredients();
pie2.listIngredients();
// Redefine the delegate method to test that it is actually on the prototype
Object.getPrototypeOf(pie1).listIngredients = function() {
console.log('Chicken, Pastry, Sauce - What more could you want?');
};
// Call the same method to test that all methods calls are delegating
pie1.listIngredients();
pie2.listIngredients();
Methods are just the start though; we still need our factories to create state.
We can do this by passing in an object with the data we want to be copied onto each object instance created from the factory.
// A factory function that will generate factories for us
function createFactory(proto) {
// Return a function that will generate objects based on the given prototype
return function() {
// Create a new object with the common methods
var obj = Object.create(proto.methods || {});
// Copy the state onto the new object
for (var key in proto.state || {}) {
obj[key] = proto.state[key];
}
return obj;
};
}
// Generate a chicken pie factory
var createChickenPie = createFactory({
// Set some state for each pie
state: {
isOpen: false
},
// Attach some delegate methods to the methods key
methods: {
listIngredients: function() {
console.log('Chicken, Pastry, Sauce');
},
divideThePie: function(slices) {
console.log((Math.PI * 2) / slices);
}
}
});
// Manufacture some chicken pies
var pie1 = createChickenPie();
var pie2 = createChickenPie();
// Check the state
console.log(pie1.isOpen);
console.log(pie2.isOpen);
// Set the state
pie1.isOpen = true;
// Check the state
console.log(pie1.isOpen);
console.log(pie2.isOpen);
And lastly we need it to accept closures in order to facilitate encapsulation.
This is done by passing in an array of closures which are then looped over and called using the built in JavaScript call()
method to set the context of this
to the newly created object.
// A factory function that will generate factories for us
function createFactory(proto) {
// Return a function that will generate objects based on the given prototype
return function() {
// Create a new object with the common methods
var obj = Object.create(proto.methods || {});
// Call each closure setting the `this` context to the new object
(proto.closures || []).forEach(function(closure) {
closure.call(obj);
});
// Copy the state onto the new object
for (var key in proto.state || {}) {
obj[key] = proto.state[key];
}
return obj;
};
}
// Generate a chicken pie factory
var createChickenPie = createFactory({
// Set some state for each pie
state: {
isOpen: false
},
// Attach some delegate methods to the methods key
methods: {
listIngredients: function() {
console.log('Chicken, Pastry, Sauce');
},
divideThePie: function(slices) {
console.log((Math.PI * 2) / slices);
}
},
// Set some closure to facilitate encapsulation
closures: [
function() {
var _useByDate = new Date();
_useByDate.setDate(_useByDate.getDate() + 7);
this.getUseByDate = function() {
return _useByDate;
};
this.setUseByDate = function(daysDifference) {
_useByDate.setDate(_useByDate.getDate() + daysDifference);
};
}
]
});
// Manufacture some chicken pies
var pie1 = createChickenPie();
var pie2 = createChickenPie();
// Check the use by date
console.log(pie1.getUseByDate());
console.log(pie2.getUseByDate());
// Set the use by date
pie1.setUseByDate(-2); // Minus 2 days from current use by date
// Check the use by date
console.log(pie1.getUseByDate());
console.log(pie2.getUseByDate());
This factory does everything we need it to do but could be improved a lot.
For example we could allow the generated factories to take in an options object that could override state on a per object basis on creation.
We could also create/utilise a well written extend function to properly copy state when the state might be an object, because as it stands this code doesn't copy objects, it just passes a reference since that is how objects are treated in assignment in JavaScript by default.
But aside from minor tweaks and improvements this method of generated factories cuts out a lot of redundant code and cuts down on the size of our scripts.