comments

Safer JavaScript Object Composition With Traits and Traits.js

The Mastering the Arcane Art of JavaScript-mancy series are my humble attempt at bringing my love for JavaScript to all other C# developers that haven’t yet discovered how awesome this language and its whole ecosystem are. These articles are excerpts of the super duper awesome JavaScript-Mancy book a compendium of all things JavaScript for C# developers.

From Mixins to Traits

In the last article of the series you learned about mixins and how you can use them to encapsulate reusable units of behavior that you can compose with your domain objects or classes.

Mixins while awesome have some limitations. In particular, conflicting mixin methods and properties are overwritten when using Object.assign. Moreover, you don’t get any warning when this happens. Updating a mixin with new functionality at some later point in time can inadvertently change the behavior of some of your objects.

Traits offer a solution to this problem by providing a safer and more structured way to do object composition.

Traits

Traits were developed as a response to the problems that exist in more traditional OOP practices like classical inheritance, multiple inheritance and mixins:

  • Classes in classical inheritance perform two distinct functions with conflicting goals. They work as factories for objects and as a mechanism of code reuse through inheritance. The first goal requires a class to be complete so that you can instantiate objects using it whilst code reuse shines when you have small reusable units. Both of these goals conflict with each other as complete classes beget larger reusable units, and small reusable units beget incomplete classes. As a result, you can use inheritance as a method of code reuse where you inherit everything, you can incur in code duplication between different classes, or require a lot of boilerplate to delegate to other classes.
  • Multiple inheritance improves the code reuse factor from the single-class inheritance approach but comes with its own host of problems. With multiple inheritance you can define smaller classes and make new classes reuse functionality from these smaller units. However, problems arise as several paths of the inheritance tree can provide conflicting functionality and overriding features with super can be ambiguous (Which super class where you referring to, my dear?).
  • Mixins excel at code reuse by defining small reusable units that you can compose with your existing classes and objects. Unlike classes a mixin doesn’t have the goal of being a factory for objects and therefore can be incomplete (and small and focused). On the minus side, because of the mechanism used to compose classes and objects with mixins, there are no guarantees that the composed class will meet the mixin requirements nor that mixins will conflict with each other in unexpected ways. Furthermore, there are no warnings when mixin features conflict with each other.

Traits attempt to solve these problems by providing a way to reuse code and behavior just like we do with mixins but in a safer fashion that will let us:

  • handle conflicts between traits and be warned when conflicts occur
  • define requirements in our traits that must be satisfied before certain features can be used

Let’s see how you can get started using traits in JavaScript.

Traits with traits.js

You can experiment with all examples in this article directly within this jsBin.

We are going to be using traits.js, a traits JavaScript library for the remainder of the article. Note that there are other trait implementations in JavaScript like light-traits and simple-traits so you can pick the one you like the most when you are ready to experiment yourself.

Trait.js is an open source library that brings the beauty of traits into JavaScript. Using traits.js we can define reusable pieces of behavior - traits - and then compose them together to build objects. Instead of starting off with a class of object you compose an object from scratch using traits.

Let’s imagine that we want to represent the ability of being positioned in a two-dimensional space using a trait TPositionable. Traits.js lets us define a new trait by using a factory function Trait and passing in an object that contains the behavior encapsulated by the trait:

Much in the same way that we use the letter I in front of interfaces in C#, it is common to use the letter T as a convention when defining traits.

let TPositionable = Trait({
  x: 0,
  y: 0,
  location() {
    console.log(`${this} is calmly resting at (${this.x}, ${this.y})`)
  },
})

In this particular case, the TPositionable trait is composed by two properties x and y that represent a position, and a method location which prints the current position.

Now that we have defined our first trait we can express new objects in terms of that trait. For instance a very sparse minion:

function MinionWithPosition() {
  var methods = {
    toString() {
      return 'minion'
    },
  }
  var minion = Object.create(
    /* prototype */ methods,
    /* traits */ TPositionable
  )
  return minion
}

In this example we have a factory function MinionWithPosition that creates minions with the ability of being positioned. We use Object.create to create an instance of a minion with a prototype that contains a single method toString and the TPositionable trait. This is interesting because it highlights the fact that we can combine JavaScript’s prototypical inheritance with traits.

We can verify that indeed the resulting minion of using this factory function works as we would expect:

var minionWithPosition = MinionWithPosition()
minionWithPosition.location()
// => minion is calmly resting at (0, 0)

A minion that rests in the same place for eternity is not very useful. Let’s see how we can truly tap into the power of traits by giving this minion more behaviors.

Composing Traits

Let’s define a new trait that will represent the behavior of moving from one place to another TMovable:

let TMovable = Trait({
  x: Trait.required,
  y: Trait.required,
  movesTo(x, y) {
    console.log(`${this} moves from (${this.x}, ${this.y}) to (${x}, ${y})`)
    this.x = x
    this.y = y
  },
})

This new trait is going to require two properties x and y - it doesn’t make sense for someone to move if you cannot be in any position - and a movesTo method to perform the actual moving around. Notice how Trait.js lets us define required properties by using the static member Trait.required. These required properties will be factored later when we try to instantiate a new object.

Now that we have two traits let’s compose them to create a more useful minion. You can compose traits using the Traits.compose method which will result in a new composite trait:

function MovingMinion() {
  var methods = {
    toString() {
      return 'moving minion'
    },
  }
  var minion = Object.create(
    /* prototype */ methods,
    Trait.compose(TPositionable, TMovable)
  )
  return minion
}

And we can see how a moving minion can be positioned and also move around:

var movingMinion = MovingMinion()
movingMinion.location()
// => moving minion is calmly resting at (0, 0)
movingMinion.movesTo(2, 2)
// => moving minion moves from (0, 0) to (2, 2)
movingMinion.location()
// => moving minion is calmly resting at (2, 2)

In the previous example we used the new composite trait implicitly in the Object.create method which made it pass by a little bit unnoticed. Know that you can save composite traits for later and compose them with other traits:

var TPositionableAndMovable = Trait.Compose(TPositionable, TMovable);
var TDrawable = Trait({...drawing behavior...});
var T2DCapable = Trait.Compose(TPositionableAndMovable, TDrawable);
// etc

What Happens When You Miss Required Properties?

Let’s try creating a new object with the TMovable trait that doesn’t meet its requirements. A ConfusedMinion which can move around but doesn’t know where it is exactly:

function ConfusedMinion() {
  var methods = {
    toString() {
      return 'confused minion'
    },
  }
  var minion = Object.create(/* prototype */ methods, TMovable)
  return minion
}

var confusedMinion = ConfusedMinion()
confusedMinion.movesTo(1, 1)
// => confused minion moves from (undefined, undefined) to (1, 1);
// => TypeError: Cannot assign to read only property 'x' of [object Object]

As you can appreciate in this example when the requirements of a specific trait haven’t been met you get some nice feedback.

Calling Object.create with a trait that misses required properties results in an object that has these requirements as read-only properties. If you try to set these properties to a new value you will get an exception which will warn you about the fact that your object is not correctly composed. This is a great improvement from mixins where missing expected properties could result in unexpected side-effects.

Assigning to Read-only Properties Only Throws in Strict Mode

Notice that you need to enable strict mode for read-only properties to throw exceptions when assigning values to them. Otherwise the assign operation will just fail silently.

Resolving Name Conflicts

Unlike mixins which only support linear composition where later mixins overwrite the previous ones, Traits composition order is irrelevant. With traits, conflicting methods or properties must be resolved explicitly.

Let’s imagine that we want to be able to position our minions in a three dimensional space. We define a TPositionable3D trait like this:

let TPositionable3D = Trait({
  x: 0, // conflict
  y: 0, // conflict
  z: 0,
  location() {
    // conflict
    console.log(`${this} is calmly resting at 
(${this.x}, ${this.y}, ${this.z})`)
  },
})

Since we want to retain the ability to position our minion in a two dimensional space (we want to be able to switch between a map view and a real-world view) we define our new minion like this:

function ConflictedMinion() {
  var methods = {
    toString() {
      return 'conflicted minion'
    },
  }
  var minion = Object.create(
    /* prototype */ methods,
    Trait.compose(TPositionable, TPositionable3D)
  )
  return minion
}

If we now attempt to create a conflicted minion and access any of its conflicting properties we will get an exception:

var conflictedMinion = ConflictedMinion()
conflictedMinion.location()
// => Error: Conflicting property: location

This will provide us with great feedback when there are name collisions between our traits properties and methods, again an important advantage over mixins. This behavior will be particularly helpful when updating an existing trait results in name collisions within existing objects within your application (which otherwise would have gone unnoticed).

Traits provide different ways in which you can resolve name conflicts:

  • aliasing or renaming properties: when you want to conserve the functionality in either of the conflicting traits. Renaming the conflicting properties will result in objects containing both the original properties plus the renamed ones.
  • excluding properties: when you don’t care about a particular trait functionality.
  • overriding properties: when you want a trait to completely override another.

Trait.js offers the Trait.resolve function to help you resolve name conflicts. You can rename a property by using this function to map a property to another name:

function AliasedMinion(){
  var methods = {
    toString() { return 'aliased minion'; }
  };
  var minion = Object.create(/* prototype */ methods,
      Trait.compose(TPositionable,
                    Trait.resolve(/* mappings * /{
                                   x: 'x3d',
                                   y: 'y3d',
                                   location: 'location3d'
                                  }, TPositionable3D)));
  return minion;
}

Trait.resolve takes two arguments, first an object that describes the conflicting property mappings and second the trait whose properties we want to rename. After explicitly resolving the conflicts we can instantiate a new minion without problems:

var aliasedMinion = new AliasedMinion()
aliasedMinion.location3d()
// => aliased minion is calmly resting at (0, 0, 0)
aliasedMinion.location()
// => aliased minion is calmly resting at (0, 0)

Using this renaming approach the object composed from the traits will keep all the properties from the original traits:

console.log(aliasedMinion)
/* => [object Object] {
  toString: function toString() {
      return 'aliased minion';
    },
  x: 0,
  x3d: 0,
  y: 0,
  y3d: 0,
  z: 0,
}
*/

Note how the methods defined by the traits location and location3d do not appear when logging the object. The reason for this is that methods created through traits are not enumerable, that is, they cannot be enumerated by using the for/in loop. This can be helpful when you want to enumerate the properties of an object and you are only interested about its data members.

We can verify that both of these methods location and location3d are part of the aliasedMinion object:

console.log(aliasedMinion.location)
// => function location() {
//  console.log(this + " is calmly resting at (" + // this.x + ", " + this.y + ")");
// }
console.log(aliasedMinion.location3d)
// => function location() {
//  // conflict
//  console.log(this + " is calmly resting at (" + // this.x + ", " + this.y + ", " + this.z + ")");
// }

Which brings us to a very important thing to notice: renaming a property doesn’t rename the property within the body of a function. You can appreciate this if you take a look at the body of location3d which still refers to this.x and this.y.

Alternatively you can exclude specific properties using Trait.resolve and setting the value of a property mapping to undefined:

function LeanMinion(){
  var methods = {
    toString() { return 'lean minion'; }
  };
  var minion = Object.create(/* prototype */ methods,
      Trait.compose(TPositionable, TMovable
                    Trait.resolve({x: undefined,
                               y: undefined,
                               location: 'location3d'
                             }, TPositionable3D)));
  return minion;
}

Creating a lean minion where the x and y properties of the TPositionable3D properties have been excluded results in a leaner object:

var leanMinion = LeanMinion()
leanMinion.location3d()
// => lean minion is calmly resting at (0, 0, 0)
leanMinion.location()
// => lean minion is calmly resting at (0, 0)

console.log(leanMinion)
/*
[object Object] {
  toString: function toString() {
      return 'lean minion';
    },
  x: 0,
  y: 0,
  z: 0
}
*/

Finally you can use Trait.override to override conflicting properties between traits. Trait.override works in a similar way to Object.assign but the precedence is taken from left to right. That is, the properties within the first trait will override those of the second trait, the properties within the second trait will override those of the third trait and so on:

function OverridenMinion() {
  var methods = {
    toString() {
      return 'overriden minion'
    },
  }
  var minion = Object.create(
    /* prototype */ methods,
    Trait.compose(TMovable, Trait.override(TPositionable3D, TPositionable))
  )
  return minion
}

In the resulting minion all properties from TPositionable have been overwritten by TPositionable3D:

var overridenMinion = OverridenMinion()
overridenMinion.location()
// => overriden minion is calmly resting at (0, 0, 0)
overridenMinion.movesTo(1, 2)
// => overriden minion moves from (0, 0) to (1, 2)
overridenMinion.location()
// => overriden minion is calmly resting at (1, 2, 0)

console.log(overridenMinion)
/* => 
[object Object] {
  toString: function toString() {
      return 'overriden minion';
    },
  x: 1,
  y: 2,
  z: 0
}

*/

Traits and Data Privacy

You can achieve data privacy with traits using closures. You will need to wrap your traits in functions just like we did with functional mixins:

let TPositionableFn = function(state) {
  var position = state.position
  return Trait({
    location() {
      console.log(`${this} is calmly resting at (${position.x}, ${position.y})`)
    },
  })
}

let TMovableFn = function(state) {
  var position = state.position
  return Trait({
    movesTo(x, y) {
      console.log(
        `${this} moves from (${position.x}, ${position.y}) to (${x}, ${y})`
      )
      position.x = x
      position.y = y
    },
  })
}

The positionable and movable traits define a single method each: location and movesTo. These methods enclose the variable state that is going to be passed to either function as an argument and which will represent the private state of an object.

Having defined these trait factories TPositionableFn and TMovableFn we can now represent a new kind of minion in terms of them:

function PrivateMinion() {
  var state = { position: { x: 0, y: 0 } },
    methods = { toString: () => 'private minion' }

  var minion = Object.create(
    /* prototype */ methods,
    Trait.compose(TPositionableFn(state), TMovableFn(state))
  )
  return minion
}

The PrivateMinion is going to have a series of private members defined by the state variable. When instantiating a new object, the factory method will share this private state with the traits but it won’t let it be accessible from the outside world:

var privateMinion = PrivateMinion()
// we can access the public API as usual
privateMinion.movesTo(1, 1)
// => private minion moves from (0, 0) to (1, 1)
privateMinion.location()
// => private minion is calmly resting at (1, 1)

// but the private state can't be accessed
console.log(privateMinion.state)
// => undefined

Using closures with traits we get:

  1. true data privacy and the ability to use private members within an object and its traits
  2. required properties and name conflict handling for the public interface of an object

You may be wondering what happens with symbols. Well, unfortunately the current implementation of traits.js does not support symbols.

High Integrity Objects With Immutable Traits

Up until this point we have instantiated our objects using the Object.create method and passing a prototype and a trait (or a composite trait) as arguments. This results in a new object with the following characteristics:

  • If all requirements are met and there are no conflicts the resulting object will contain all properties and methods defined within the traits and will have as prototype whichever object we have passed to Object.create.
  • If there are properties that are required but haven’t been satisfied the resulting object has these requirements as read-only properties. Attempting to modify these results in an exception.
  • If there are unresolved naming conflicts the resulting object throws an exception when conflicting properties or methods are accessed.

We are getting much better feedback about the consistency of our composed object than when we used mixins but it could be better: We could get that feedback much sooner. Like directly when creating the object and not when accessing inconsistent properties or methods.

Trait.js offers another method Trait.create that lets you instantiate high integrity objects. Objects created using Trait.create will:

  • throw an exception if there are requirements that haven’t been satisfied
  • throw an exception if there are unresolved naming conflicts
  • have all their methods bound to themselves
  • be immutable

Let’s use Trait.create with some of the traits we defined previously in this article.

function ImmutableMinionWithPosition() {
  var methods = {
    toString() {
      return 'minion'
    },
  }
  var minion = Trait.create(/* prototype */ methods, /* traits */ TPositionable)

  return minion
}

var immutableMinion = new ImmutableMinionWithPosition()
immutableMinion.location()
// => minion is calmly resting at (0, 0)

The resulting immutableMinion is an immutable object. Attempting to change, delete or add new properties will result in an exception (in strict mode otherwise it will fail silently):

immutableMinion.x = 10
// => TypeError: Cannot assign to read only property 'x

delete immutableMinion.x
// => TypeError: Cannot delete property 'x'

immutableMinion.health = 100
// => TypeError: Can't add property health, object is not extensible

Likewise if we attempt to create an object with missing requirements Trait.create will let us know immediately by throwing a composition exception:

function ConfusedMinionThatThrows() {
  var methods = {
    toString() {
      return 'confused minion'
    },
  }

  // The TMovable trait requires two properties: x and y
  var minion = Trait.create(/* prototype */ methods, TMovable)
  return minion
}

var confusedMinionThatThrows = ConfusedMinionThatThrows()
// => Error: Missing required property: x

Which will also be the case when trying to create an object with unresolved conflicts:

function ConflictedMinionThatThrows() {
  var methods = {
    toString() {
      return 'conflicted minion'
    },
  }
  var minion = Trait.create(
    /* prototype */ methods,
    Trait.compose(TPositionable, TPositionable3D)
  )
  return minion
}

var conflictedMinionThatThrows = ConflictedMinionThatThrows()
// => Error: Remaining conflicting property: location

Trait.create offers a better developer experience than Object.create and helps you create high integrity objects that are immutable. But how do you build an application if all your objects are immutable? How can you make a minion move if you cannot change its state? The answer is that you use other mechanisms to manage state than what we are accustomed to in traditional object-oriented programming. In Functional Programming: Immutability (a future article in these series) we will do a deep dive into immutability, its advantages, uses cases and how you can use it in your applications.

Below you can find a summarized comparison between using Object.create and Trait.create:

Object.create Trait.create
Can create objects even if there are unmet requirements or unresolved conflicts. Cannot create objects when there are unmet requirements or unresolved conflicts.
Unmet requirements result in read-only properties. Read-only properties throw when you try to change them in strict mode. Unmet requirements cause an exception as soon as we try to instantiate an object.
Properties with unresolved conflicts throw an exception when accessed. Unresolved conflicts cause an exception as soon as we try to instantiate an object.
The object created doesn't have its method bound The object created has all its methods bound to itself.
The object created can be modified and augmented with new properties. The object created is immutable. You cannot augment it with new properties, remove properties nor modify existing ones.

Traits vs Mixins

Mixins Traits
Class-free inheritance based on object composition via Object.assign. Let's you encapsulate functionality and behavior, and easily reuse them. Class-free inheritance based on trait composition. Let's you encapsulate functionality and behavior, and easily reuse them.
Mixins don't have a way to express requirements. A mixin may expect a property or method in the composed object but it doesn't have a way to represent it. If a requirement is not met, unpredictable side-effects may occur without a warning. Traits can express that they require specific properties or methods for functioning. Failing to meet requirements will results in errors being thrown either by trying to assign to an unexisting required property or upon object creation (Trait.create).
Only allow linear composition. Later composed mixins overwrite previous mixins. Can be composed freely because it requires that you resolve any conflict explicitly. Conflicts can be resolved by renaming, excluding properties or by overriding traits. Unresolved conflicts will result in exceptions being throw when accessing conflicting properties or methods, or on object creation (with Trait.create).
Support data privacy with closures and symbols. Support data privacy with closures and symbols. Trait.js doesn't support symbols but that's more of an implementation details than traits themselves not supporting symbols.
Object mixins can lead to state being coupled between different objects composed from the same mixin. Functional mixins provide a solution to this problem by doubling as an object factory and ensuring that each new object is composed with new state. Traits can also lead to state being coupled between composed objects. In order to avoid that, wrap your trait inside a trait factory function. That will ensure that new objects are composed from new state.
Mixins usually extend existing objects or classes. Traits create new objects from scratch by composing many traits together instead of extending existing objects or classes.
Supports the easy creation of high integrity objects using Trait.create.

Concluding

Traits are a class-free object oriented programming alternative to mixins. Just like mixins they encapsulate reusable pieces of behavior that can be composed together to create complex objects. They are an improvement over mixins because they let you express requirements within your traits and actively resolve conflicts. Both of these features result in code that is less error prone because composition mistakes don’t fail silently and cause unwanted side-effects like with mixins.

Traits.js is a javascript library that brings traits to JavaScript. It lets you define traits via the Trait factory method, compose traits with Trait.compose, define requirements using Trait.required and resolve conflicts via Trait.resolve.

Traits.js offers two ways to instantiate objects from traits: Object.create and Trait.create. The first one, which is native to JavaScript, creates vanilla JavaScript objects that can be mutated and augmented. With Object.create unmet requirements result in read-only properties and accessing properties with unresolved conflicts results in exceptions. Trait.create offers a high integrity alternative to Object.create that throws on object creation when requirements are missing or there are unresolved conflicts. Trait.create returns immutable objects which we cannot augment with new properties and whose properties cannot be changed nor deleted.

Learn More About Traits in These Papers

Interested in Learning More JavaScript? Buy the Book!

Are you a C# Developer interested in learning JavaScript? Then take a look at the JavaScript-mancy book, a complete compendium of JavaScript for C# developers.

a JavaScriptmancy sample cover

Have a great week ahead!! :)

More Articles In This Series