Traits - nodirt/defineClass GitHub Wiki

Traits

Traits are the continuation of OOP and were borrowed from the Scala programming language. Here is a quote from Martin Ordersky's "Programmin in Scala" book:

Traits are a fundamental unit of code reuse in Scala. A trait encapsulates method and field definitions, which can then be reused by mixing them into classes. Unlike class inheritance, in which each class must inherit from just one superclass, a class can mix in any number of traits.

defineClass implements traits for JavaScript.

How traits work

A trait looks like a class definition except it is defined using defineClass.trait function:

// define a trait
var Bell = defineClass.trait({
  ring: function () {
    console.log("Ringing!")
  }
});

A class can mix a trait. In this case all trait's members are imported into the class.

var Phone = defineClass({
  _super: [Device, Bell]
  // other members
});

var phone = new Phone();
phone.ring(); // Ringing!

Roughly speaking, internally defineClass generates a temporary class with the trait definition inherited from the base class, and then inherits your class from the temporary class:

var tempClass = defineClass({
  _super: Device,
  ring: function () {
    console.log("Ringing!")
  }
});

var Phone = defineClass({
  _super: tempClass,

  sendMessage: function (text) {
    console.log("Sending SMS");
    console.log(text);
  }
});

Since it is based on inheritance, everything that works in inheritance applies to this.

Calling class methods in traits

Since trait methods are called in the context of a class instance, a trait method can call a class method as it is a part of the trait definition.

// define a trait that defines a new method "sendFormattedMessage"
// that uses another method "sendMessage"
var FormattedMessageSender = defineClass.trait({
  sendFormattedMessage: function (subject, body) {
    // this trait assumes that there is a "sendMessage" method in the class that mixes the trait in
    this.sendMessage("SUBJECT: " + subject + "\nBODY: " + body);
  }
});

var Phone = defineClass({
  // apply a trait
  _super: [Device, FormattedMessageSender],

  sendMessage: function (text) {
    console.log("Sending SMS:");
    console.log(text);
  }
});

var phone = new Phone(1000);
phone.sendFormattedMessage("Meetup", "What about 5pm?");
// Output:
//   Sending SMS:
//   SUBJECT: Meetup
//   BODY: What about 5pm?

You could have another class Telegraph with sendMessage method and mix the FormattedMessageSender in it. The same trait can be used in multiple classes while keeping its implementation separately:

var Telegraph = defineClass({
  _super: FormattedMessageSender,
  sendMessage: function (text) {
    console.log("Sending letter:");
    console.log(text);
  }
});

Overriding base class methods with traits

Since internally trait application is based on inheritance, a trait method can override a base method and call it as this._super:

// define a trait that overrides "turnOn" method and prints "Playing sound..."
var TurnOnWithSound = defineClass.trait({
  turnOn: function () {
    this._super();
    console.log("Playing sound...");
  }
}); 

var Phone = defineClass({
  _super: [Device, TurnOnWithSound]
});

var phone = new Phone(1000);
phone.turnOn();
// Output:
//   Turning on...
//   Playing sound...

This powerful technique allows implementing behavior aspects as traits and apply them to different classes.

Trait order in _super

Multiple traits can be specified in _super and the order of traits is important because it controls the trait application order:

// define one more trait that overrides "turnOn"
var TurnOnWithSplash = defineClass.trait({
  turnOn: function () {
    this._super();
    console.log("Showing splash...");
  }
}); 

// apply TurnOnWithSound first, then TurnOnWithSplash 
var Phone = defineClass({
  _super: [Device, TurnOnWithSound, TurnOnWithSplash]
});

var phone = new Phone(1000);
phone.turnOn();
// Output:
//   Turning on...
//   Playing sound...
//   Showing splash...

And here is a different trait order:

// apply TurnOnWithSplash first, then TurnOnWithSound
var Phone2 = defineClass({
  _super: [Device, TurnOnWithSplash, TurnOnWithSound]
});

var phone = new Phone2(1000);
phone.turnOn();
// Output:
//   Turning on...
//   Showing splash...
//   Playing sound...

Phone and Phone2 classes apply the traits in different order and so the order of printed lines is different.

A trait can import other traits

Traits can import other traits by specifying them in the _super field. When such a trait is applied, its super traits are applied first. This process is recursive.

var EnhancedTurnOn = defineClass.trait({
  _super: [TurnOnWithSound, TurnOnWithSplash],

  // you can override the method further
  turnOn: function () {
    console.log("Starting turnOn")
    this._super();
    console.log("Finishing turnOn")
  }
});

var Phone = defineClass({
  _super: [Device, EnhancedTurnOn]
});

var phone = new Phone2(1000);
phone.turnOn();
// Output:
//   Starting turnOn
//   Turning on...
//   Playing sound...
//   Showing splash...
//   Finishing turnOn

Trait is a function

A trait itself is a function that receives a class/prototype/trait, adds/customizes its behavior by applying itself to it and returns the result.

var Phone = defineClass({ _super: Device });
var PhoneWithSound = TurnOnWithSound(Phone);
var phone = new PhoneWithSound(1000);
phone.turnOn();
// Output:
//   Starting turnOn
//   Playing sound...

Another way to apply a trait is to call the decorate method of a class/trait that applies all argument functions to the itself:

var PhoneWithSound = Phone.decorate(TurnOnWithSound);
var PhoneWithSoundAndSplash = Phone.decorate(TurnOnWithSound, TurnOnWithSplash);

Why it is important

There are two serious reasons to use traits:

  1. Traits allow extracting common functionality of different class hierarchies to a single object and then it to multiple classes.
  2. Traits allow separating different concerns, thus they are a part of AOP.