// lib/server/computer.js
'use strict';
module.exports = Computer;
// Constructor.
function Computer() {
}
// Property.
Object.defineProperty(Computer.prototype, 'host', {
get: function() { return this._host; },
set: function(host) { this._host = host; }
});
// Method.
Computer.prototype.compute = function() {
// ...
}
You can use ES6 class syntaxic sugar if you prefer but be aware of the browser/node.js version compatibility.
The definition of a class is really easy, you just have to declare a function and return it with module.exports
as in any other node.js file.
You can, then, define some properties and some methods on this function as shown in this example.
The identifier logical name of your class is
computer
. Identifier logical names are built from paths:
/lib/server/class.js
=>class
/lib/server/foo/bar.js
=>foo.bar
/lib/common/class.js
=>class
The standard module scoping is applying on the classes names:/node_modules/dependency/lib/common/class.js
=>dependency:class
/node_modules/dependency/lib/client/foo/bar.js
=>dependency:foo.bar
// lib/server/super-computer.js
'use strict';
module.exports = SuperComputer;
// Constructor.
function SuperComputer() {
// Call parent constructor.
SuperComputer.Parent.call(this);
}
// Define inherited class
// (use the identifier logical name of the parent class).
SuperComputer.defineExtendedClass('computer');
// Method.
SuperComputer.prototype.compute = function() {
// Call parent method.
SuperComputer.Parent.prototype.compute.call(this);
// ...
}
You can inherit from a class using the method defineExtendedClass
on the child class and passing the declared name of the class.
It is possible to call the parent constructor using .Parent.call(this, ...);
in the constructor of the child class.
You can use non-instantiable class:
// lib/server/abstract-computer.js
'use strict';
module.exports = AbstractComputer;
// Constructor.
function AbstractComputer() {
Object.hasMethod(this, 'compute', true);
}
// Define the class as an abstract class.
AbstractComputer.defineAsAbstract();
// Abstract method.
AbstractComputer.prototype.compute = null;
The method call defineAsAbstract
on the class will define the class as abstract and prevent it to be instantiated.
Object.hasMethod(this, 'compute', true);
called in the constructor forces child classes to define the methodcompute
. This can be used to define not instantiated protected-like method.
Like it is explained in the concepts, interfaces are really important in object architectures.
In Danf, interfaces assume two roles as a good native object langage would do:
- Ensure the respect of the contract they define.
- Create a scope preventing to call any method not defined by the interface (used for low coupling).
Danf highly encourages their use and provides an easy way to define them:
// config/server/config/interfaces.js
'use strict';
module.exports = {
computer: {
methods: {
/**
* Compute.
*
* @param {boolean_array} operations The boolean operations.
* @return {boolean} The result of the computing.
*/
compute: {
arguments: ['boolean_array/operations'],
returns: 'boolean'
}
},
getters: {
/**
* The host.
*
* @return {string}
*/
host: 'string',
/**
* The processor.
*
* @return {processor}
*/
processor: 'processor'
},
setters: {
/**
* The host.
*
* @param {string}
*/
host: 'string',
/**
* The processor.
*
* @param {processor}
*/
processor: 'processor'
}
},
processor: {
methods: {
/**
* Process.
*
* @param {boolean} operation The boolean operation.
* @param {boolean} previousState The previous state.
* @return {boolean} The result of the processing.
*/
process: {
arguments: ['boolean/operation', 'boolean|undefined/previousState'],
returns: 'boolean'
}
}
},
sequencer: {
methods: {
/**
* Wake.
*/
wake: {
}
}
}
};
You can define methods, getters and/or setters. You can specify the same types as used for the contracts in the configuration but you can also specify some interface types.
You can define a multitype field string|number
.
So, an optional string can be noted string|undefined
or string|null
.
...
can be used to tell that a variable number of arguments of this type can appear:
string...
means 1 or N string arguments.string...|null
means 0 or N string arguments. You can use this many times in the same list of arguments and at any places.
This will works for instance: arguments: ['processor', 'string...|number...', 'number...|null', 'undefined|boolean']
.
To tell that a class implements an interface you just have to call defineImplementedInterfaces
on the class and pass it a list of the implemented interfaces. This is transitive, you do not have to call it in child classes.
// lib/server/computer.js
'use strict';
module.exports = Computer;
/**
* Constructor.
*/
function Computer() {
}
// Define implemented interfaces.
Computer.defineImplementedInterfaces(['computer']);
// Define a dependency which must be a string.
Computer.defineDependency('_host', 'string');
// Define a dependency which must be an instance of the interface "processor".
Computer.defineDependency('_processor', 'processor');
/**
* @interface {computer}
*/
Object.defineProperty(Computer.prototype, 'host', {
get: function() { return this._host; },
set: function(host) { this._host = host; }
});
/**
* @interface {computer}
*/
Object.defineProperty(Computer.prototype, 'processor', {
get: function() { return this._processor; },
set: function(processor) { this._processor = processor; }
});
/**
* @interface {computer}
*/
Computer.prototype.compute = function(operations) {
var result = true;
for (var i = 0; i < operations.length; i++) {
result = this._processor.process(operations[i], result);
}
return result;
}
// lib/server/processor.js
'use strict';
module.exports = Processor;
/**
* Constructor.
*/
function Processor() {
}
// You can define several interfaces of course.
Processor.defineImplementedInterfaces(['processor', 'sequencer']);
/**
* @interface {processor}
*/
Processor.prototype.process = function(operation, previousState) {
return operation && previousState;
}
/**
* @interface {sequencer}
*/
Processor.prototype.wake = function() {
// ...
}
/**
* @api protected
*/
Processor.prototype.wait = function() {
// ...
}
You will see in the section dependency injection how to inject a processor in a computer but imagine that an instance of Processor
has been injected in an instance of Computer
here.
The call of defineDependency
on the class Computer
(Computer.defineDependency('_processor', 'processor');
) is going to allow:
- To check that the processor is implementing the interface
processor
. - To create a proxy on the processor preventing to call methods, getters and setters which are not defined by the interface.
In this example, the instance of Processor
implements the interface processor
(Processor.defineImplementedInterfaces(['processor']);
) and the instance of Computer
will be able to call processor.compute(...)
but not processor.wake()
or processor.wait()
(because the interface processor
only define the method compute
).
An interface can inherit from another:
// config/server/config/interfaces.js
'use strict';
module.exports = {
computer: {
methods: {
compute: {
}
}
},
advancedComputer: {
extends: 'computer',
methods: {
computeOnCore: {
}
}
}
};
You just have to set the name of the extended interface in the node extends
of the extender interface.
The standard module scoping is applying on the interface names:
- The name of the interface
int
of the dependencyfoo
(/node_modules/foo
) seen by your module isfoo:int
.- The name of the interface
int
of the dependencybar
of the dependencyfoo
(/node_modules/foo/node_modules/bar
) seen by your module isfoo:bar:int
.- The name of the interface
int
of the dependencybar
of the dependencyfoo
(/node_modules/foo/node_modules/bar
) seen byfoo
isbar:int
.