Monday, April 24, 2017

Patterns for Object Inheritance in JavaScript ES2015

With the long-awaited arrival of ES2015 (formerly known as ES6), JavaScript is equipped with syntax specifically to define classes. In this article, I’m going to explore if we can leverage the class syntax to compose classes out of smaller parts.

Keeping the hierarchy depth to a minimum is important to keep your code clean. Being smart about how you split up classes helps. For a large codebase, one option is to create classes out of smaller parts; composing classes. It’s also a common strategy to avoid duplicate code.

Imagine we’re building a game where the player lives in a world of animals. Some are friends, others are hostile (a dog person like myself might say all cats are hostile creatures). We could create a class HostileAnimal, which extends Animal, to serve as a base class for Cat. At some point, we decide to add robots designed to harm humans. The first thing we do is create the Robot class. We now have two classes that have similar properties. Both HostileAnimal and Robot are able to attack(), for instance.

If we could somehow define hostility in a separate class or object, say Hostile, we could reuse that for both Cat as Robot. We can do that in various ways.

Multiple inheritance is a feature some classical OOP languages support. As the name suggests, it gives us the ability create a class that inherits from multiple base classes. See how the Cat class extends multiple base classes in the following Python code:

class Animal(object):
  def walk(self):
    # ...

class Hostile(object):
  def attack(self, target):
    # ...

class Dog(Animal):
  # ...

class Cat(Animal, Hostile):
  # ...

dave = Cat();
dave.walk();
dave.attack(target);

An Interface is a common feature in (typed) classical OOP languages. It allows us to define what methods (and sometimes properties) a class should contain. If that class doesn’t, the compiler will raise an error. The following TypeScript code would raise an error if Cat didn’t have the attack() or walk() methods:

interface Hostile {
  attack();
}

class Animal {
  walk();
}

class Dog extends Animal {
  // ...
}

class Cat extends Animal implements Hostile {
  attack() {
    // ...
  }
}

Multiple inheritance suffers from the diamond problem (where two parent classes define the same method). Some languages dodge this problem by implementing other strategies, like mixins. Mixins are tiny classes that only contain methods. Instead of extending these classes, mixins are included in another class. In PHP, for example, mixins are implemented using Traits.

class Animal {
  // ...
}

trait Hostile {
  // ...
}

class Dog extends Animal {
  // ...
}

class Cat extends Animal {
  use Hostile;
  // ...
}

class Robot {
  use Hostile;
  // ...
}

A Recap: ES2015 Class Syntax

If you haven’t had the chance to dive into ES2015 classes or feel you don’t know enough about them, be sure to read Jeff Mott’s Object-Oriented JavaScript — A Deep Dive into ES6 Classes before you continue.

In a nutshell:

  • class Foo { ... } describes a class named Foo
  • class Foo extends Bar { ... } describes a class, Foo, that extends an other class, Bar

Within the class block, we can define properties of that class. For this article, we only need to understand constructors and methods:

  • constructor() { ... } is a reserved function which is executed upon creation (new Foo())
  • foo() { ... } creates a method named foo

The class syntax is mostly syntactic sugar over JavaScript’s prototype model. Instead of creating a class, it creates a function constructor:

class Foo {}
console.log(typeof Foo); // "function"

The takeaway here is that JavaScript isn’t a class-based, OOP language. One might even argue the syntax is deceptive, giving the impression that it is.

Composing ES2015 Classes

Interfaces can be mimicked by creating a dummy method that throws an error. Once inherited, the function must be overridden to avoid the error:

class IAnimal {
  walk() {
    throw new Error('Not implemented');
  }
}

class Dog extends IAnimal {
  // ...
}

const robbie = new Dog();
robbie.walk(); // Throws an error

As suggested before, this approach relies on inheritance. To inherit multiple classes, we will either need multiple inheritance or mixins.

Another approach would be to write a utility function that validates a class after it was defined. An example of this can be found in Wait A Moment, JavaScript Does Support Multiple Inheritance! by Andrea Giammarchi. See section “A Basic Object.implement Function Check.”

Time to explore various ways to apply multiple inheritance and mixins. All examined strategies below are available on GitHub.

Object.assign(ChildClass.prototype, Mixin...)

Pre-ES2015, we used prototypes for inheritance. All functions have a prototype property. When creating an instance using new MyFunction(), prototype is copied to a property in the instance. When you try to access a property that isn’t in the instance, the JavaScript engine will try to look it up in the prototype object.

Continue reading %Patterns for Object Inheritance in JavaScript ES2015%


by Tim Severien via SitePoint

No comments:

Post a Comment