Question

I am trying to figure out if there is a better practice for initializing class members of derived classes in ES6 - in the child or the parent, and why?

For example:

Option 1:

class AbstractAnimal {
   constructor() {
       this.voice = null;
   }

   makeVoice() {
      this.voice.make(); 
   }
}

class Dog extends AbstractAnimal {
   constructor() {
       super();
       this.voice = new Bark();
   }

   onSeeFriend() {
       this.makeVoice();
   }
}

Option 2:

class AbstractAnimal {
   constructor(voice) {
       this.voice = voice;
   }

   makeVoice() {
      this.voice.make(); 
   }
}

class Dog extends AbstractAnimal {
   constructor() {
       super(new Bark());
   }

   onSeeFriend() {
       this.makeVoice();
   }
}

Obviously, there are pro's and cons in both methods. The first options spreads the initialization of members around, which makes it harder to trace. While the second option will bubble everything up to one place, but than you could end up with huge constructors taking a lot of arguments.

Would appreciate it if I could hear your thought about this. Thanks!

Was it helpful?

Solution

The voice field is a member of the AbstractAnimal class; if you access it in the Dog class using the this pointer, then, as far as the subclass is concerned, voice is part of the base class' public interface towards its subclasses. This means that there's extra coupling between the two classes, as both AbstractAnimal and Dog now rely on voice being present in the superclass, and on voice supporting particular operations. Now, this is mitigated by the fact that JavaScript supports duck-typing, but nevertheless, these are things that you as a programmer have to consider.

This coupling means that it will be difficult to change the internal structure and implementation of AbstractAnimal (e.g., remove voice, or represent it in some other way, and/or change some methods that use it) without affecting existing subclasses. This is all assuming that there's actually some role voice plays in the superclass, some behavior implemented in AbstractAnimal itself; if that's not the case, consider if AbstractAnimal should have the voice field in it at all.

Initializing superclass members via the constructor, and treating the voice field as private to the superclass, hides the internal details behind the interface of AbstractAnimal. This is generally a good idea, but again, you'll have to decide if decoupling is worth the hassle for you. If it is, you would design your classes so that each can do its job and collaborate with the other without having to rely on the other's internals. In statically typed languages, you can enforce this to some extent; here, it requires developer discipline and, in a team setting, clear communication among the team members.

Another thing you can do is to have a narrow interface for the client code (code outside of the inheritance hierarchy) and a wider interface for the subclasses (a few extra methods and maybe fields that subclasses are allowed to use, but that are not to be used from the outside). This should be somehow documented and communicated to the readers of the code. In other languages, you would do this by using the public and protected access modifiers, in ES you'd rely on naming conventions (_name) and/or certain tricks (e.g. leverage closures).

the second option will bubble everything up to one place, but than you could end up with huge constructors taking a lot of arguments.

Well, don't end up with such constructors; don't design your classes in such a way. If you have too many arguments, examine your class design more closely. Maybe your classes really do need a bunch of parameters, in which case you can bundle them in a parameter object, but more likely, your classes are trying to do too many things at once (they handle too many responsibilities), so you should break them up.

Finally, a superclass should be a behavioral abstraction of the subclasses (in the sense of LSP), you should generally avoid inheritance just so that you could inherit data members and some useful functions, although this can be handy. In the famous GoF book there's a famous line:

Favor object composition over class inheritance.

They state this as one of the principles of OO design; composition is a more flexible alternative, and designs can often be made simpler with it. They acknowledge that this is not always achievable in practice, but that more often then not, programmers overuse inheritance.

So, generally speaking, reserve inheritance for creating behavioral abstractions and subsystem facades to program against, and if you need to compose objects out of smaller parts, use traditional composition, or mixins.

Licensed under: CC-BY-SA with attribution
scroll top