What is the maximum number of arguments that a constructor should have?
The answer is 3. When the number of args exceeds 3 in a constructor, it's probably time to switch to a configuration object as a single argument. There are two-and-a-half benefits (note specificity):
-
give your constructor a chance to set defaults if they're missing, verify that specific types are defined or that specific instances of types are defined
-
OR allows you to assign a single property in the constructor to the config object and defer the integrity checks to other methods, if you wish, so you can add those later to the prototype rather than repeatedly modify the constructor (which can get quite big).
-
makes mock arguments much easier to maintain in tests where you'll be driving your constructor's integrity checks first, before adding capabilities to the prototype or inheriting from another one.
In the following, all the checks are inline or-statements; if the first condition is false, the second is executed.
function TestDriver(config) {
// if a property is not specified, supply defaults...
this.string = config.string || "defaultString";
this.regex = config.regex || "defaultRegex";
// if a property is not specified but required, throw an error
this.domNode = config.domNode || (throw new Error('domNode is required'));
// if a property is required to be a specific type...
this.fn = typeof config.fn === 'function' || (throw new Error('fn is required to be a function'));
// if a property is required to be an instance of a specific type...
this.complexObject = config.complexObject instanceof ComplexObject || (throw new Error('complexObject is required to a ComplexObject instance'));
// etc.
};
That can be pushed out to a prototype method:
function TestDriver(config) {
this.config = this.setup(config);
};
TestDriver.prototype.setup = function(config) {
// do config checks as before - but use a new object to write to, rather than "this"
var obj = {};
obj.string = config.string || default;
// etc.
// return obj if we got this far
return obj;
};
This will pass the argument checks:
var goodDriver = new TestDriver({
string: 'Hit me',
// regex is optional
domNode: domNode,
complexObject: complexObject,
fn: fn
});
This will fail - complexObject is the wrong instanceof type
var badDriver = new TestDriver({
string: 'Hit me again',
// regex is optional
domNode: domNode,
complexObject: [],
fn: fn
});
In the delegation to setup() method, you could even use a closure to keep others from hacking it accidentally on purpose,
function TestDriver(config) {
var obj = this.setup(config);
this.config = function () {
return obj ;
};
};
so that driver.config(), this.config() => always returns a compliant copy of the original config specifier.
It is for this last reason (proper inheritance of closures) that I devised my Constructor.extend() the way I did {@see constructor-api-proposal}.