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 Mammal
s 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 Dog
s, 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 Dog
s, 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 Dog
s 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.