This guide is intended to give you a brief explanation about using Traits & Talents. We are going to explore some basics features with a very simple example.
Getting Started with Traits
Traits are Composable Units of Behavior. We are going to use them as a class with methods that will be mixed into our class.
The methods specified in the Trait will be applied to the class becoming part of it as if they were defined into the class.
From a previous guide we have been exploring some designs in our example with Pirate
and Person
. Let’s refresh
how they look like:
Person.js
var cocktail = require('cocktail'),
Person = function(){};
cocktail.mix(Person, {
greeting: 'Hello',
sayHi: function(){
console.log(this.greeting + "!");
}
});
module.exports = Person;
Pirate.js
var cocktail = require('cocktail'),
Person = require('./Person'),
Pirate = function(){};
cocktail.mix(Pirate, {
'@extends': Person,
greeting: 'Ahoy'
});
module.exports = Pirate;
Imagine that we want to add a Robot
to the class model. We want the Robot to be able to say hi too. One can be tempted
to reuse the same structure in Pirate and Person but, there is no semantic relationship between a Robot
and Person. We cannot say a Robot is a Person just to inherit the sayHi mechanism. We cannot create an
abstract class to share the behavior for the same reason.
Traits solve this gap. We can create a trait to reuse the sayHi functionality. Let’s see how it works. We are going to create a new trait that we will call it Greetable and it can be defined as follows:
Greetable.js
var cocktail = require('cocktail'),
Greetable = function(){};
cocktail.mix(Greetable, {
'@requires': ['getGreeting'],
sayHi: function(){
var greeting = this.getGreeting();
console.log(greeting + "!");
}
});
module.exports = Greetable;
The code above defines a class as a Trait Greetable. It is not so different from defining a class. We have
specified our sayHi
method in this trait. This method needs to access to the greeting
variable. Since Traits
cannot define any state, we use the annotation @requires
to specify that a getGreeting
method is needed to make
the trait able to work properly.
Now we can create the Robot
class that will be composed with the trait.
Robot.js
var cocktail = require('cocktail'),
Greetable = require('./Greetable'),
Robot = function(){};
cocktail.mix(Robot, {
'@traits': [Greetable],
greeting: '01001000 01100101 01101100 01101100 01101111', //Yes, that's hello in binary :)
getGreeting: function(){
return this.greeting;
}
});
We have created a new class for Robot
. We’ve declared that our class is composed with a Trait
Greetable.
The Greetable needs a getGreeting
method so we defined one that returns our greeting property.
Refactoring our classes to use Traits
The Person and Pirate classes have the same behavior, so now we can reuse our trait functionality into them. Let’s refactor them to take advantage of the Greetable trait.
Person.js
var cocktail = require('cocktail'),
Greetable = require('./Greetable'),
Person = function(){};
cocktail.mix(Person, {
'@traits': [Greetable],
greeting: 'Hello',
getGreeting: function(){
return this.greeting;
}
});
module.exports = Person;
That’s all we need. We don’t need to change our Pirate
class since the methods defined in the trait will be
inherited from Person
.
Pirate.js
var cocktail = require('cocktail'),
Person = require('./Person'),
Pirate = function(){};
cocktail.mix(Pirate, {
'@extends': Person,
greeting: 'Ahoy'
});
module.exports = Pirate;
Let’s see now how this will work into our main file.
index.js
var Person = require('./Person'),
Pirate = require('./Pirate'),
Robot = require('./Robot'),
joe, jack, marvin;
joe = new Person();
joe.sayHi(); //will print "Hello!" in the console
jack = new Pirate();
jack.sayHi(); //will print "Ahoy!" in the console
marvin = new Robot();
marvin.sayHi(); //will print "01001000 01100101 01101100 01101100 01101111!"
Talents
We have seen how to create a Trait and how to use it into our class definition. Talents are not that different. In fact, we can reuse our Greetable trait definition as a Talent. We only need to keep in mind that our object has to be compatible with the Talent, this means that all the methods specified as required have to be part of our object.
index.js
var Person = require('./Person'),
Pirate = require('./Pirate'),
Robot = require('./Robot'),
Greetable = require('./Greetable'),
joe, jack, marvin,
dishwasher = { model: 'DISHWASHER 1000'};
joe = new Person();
joe.sayHi(); //will print "Hello!" in the console
jack = new Pirate();
jack.sayHi(); //will print "Ahoy!" in the console
marvin = new Robot();
marvin.sayHi(); //will print "01001000 01100101 01101100 01101100 01101111!"
cocktail.mix(dishwasher,{
'@talents': [Greetable],
getGreeting: function(){
return this.model;
}
});
dishwasher.sayHi(); //will print "DISHWASHER 1000!"
As you can see in the example we have defined an object dishwasher. For some reason we want to make the dishwasher
able to sayHi too. We need a getGreeting method since it is not defined in the object. To do so, we can define it
using our mix to return the dishwasher model property.
We composed our object with the Greetable trait by specifying the annotation @talents
with the desired talent.
As we said before a Trait can be considered as a Talent to compose objects.