Abstracts and Mixins

Composition > Construction

Introduction

There's a saying in OOP philosophy that goes, "we value composition over construction." What this means is that we would like to leave as many implementation details out of the created object itself and include them as they make sense. Here's an example, Let's say I have an Animal object and from that I've made the objects Cat and Dog. Cats and dogs share most behaviors in the sense that they can both extend Animal without much hassle. But then I add a Fish object. uh oh... Now I have to have intermediary objects for Mammals and Fish. Then I add a Gecko and a Frog. UGH... This could go on forever and my inheritance tree is CRAZY!


What Is Construction

So let's get a handle on what construction is first and just what it's job is. When we construct something as a base object, we should be defining what it is and not much else. So we should be assigning properties and methods that are unique to that object. In the case of Dogs, maybe we would define the sound that it makes as 'bark' and that it can wag it's tail. These are properties and actions unique to Dogs, so these can be included in the Dog object when we create it. But maybe a dog can also be walked. This behavior isn't unique to just Dogs so we wouldn't want to add it to the Dog object. We should pull that behavior into its own entity and apply it to the Dog and any other objects that might share that behavior. Let's see what that looks like.


Example

const canBeWalked = {
  walk() {
    this._happiness += 5;
  }
};

class Dog {
  constructor(name) {
    this._name = name;
    this._happiness = 50;
    Object.assign(this, canBeWalked);
  }

  // accessors and mutators, etc.
}

Mixins

In the previous example, we are including behavior in an object that is reusable. All our mixin relies on is the fact that whatever is implementing it has a _happiness property. So we are composing this object out of reusable behavior that is independent of it instead of constructing a complex single object. This way our object can remain simple and implement more complex behavior as needed.


Abstracts

Another tool we have at our disposal for this sort of thing is the idea of abstract objects. An abstract object is an object that defines some behavior and can be extended, but cannot, itself, be instantiated. So in our previous example, we have a Cat that extends the Animal object. We want to be able to make a Cat and use all of the properties and methods from Animal. But we would never want to allow the construction of a generic Animal right? The Animal is a concept and not an instance. For this we can createan abstract class. Let's take a look:


Example

class Animal {
  constructor(name) {
    this._name = name;
    if (this.constructor === Animal) {
      throw new Error(
        "This class is abstract and cannot be instantiated. Please extend"
      );
    }
  }

  get name() {
    return this._name;
  }
}

class Cat extends Animal {
  // ...
}

Abstract Classes

So in the previous example, If we try to write the following:

const noCanDo = new Animal("I may not exist!");

That doesn't work (if you couldn't already tell). Because we're checking the constructor to see if we are making a new Animal. If we are, we get an error thrown. This is a great way to create common behavior but avoid the creation of this general class.


Abstract Methods

We can do the same with methods. This is going to ensure that this method is implemented by the object extending, but is implemented as is necessary for that object. For example:

class Animal {
  // ...
  makeSound() {
    throw new Error("Abstract method must be implemented by child object");
  }
}

class Cat extends Animal {
  // ...
  makeSound() {
    return "Meow!";
  }
}

Conclusion

Using mixins and abstracts allows us to write really complex code and make some pretty intricate objects without having to make super specific, usually unextendable objects. This is a great part of the OOP philosophy to get familiar with.