Sunday, May 10, 2015

Writing Scalable Code Using Inheritance in Java

Everyone should be familiar with the old advice that you should favor composition over inheritance. However, what if you find yourself in a situation where inheritance is the best tool for the job? There are a few things you can do to keep your code maintainable.

Before we get into that I should explain a little about why you should favor composition over inheritance. Imagine you're trying to understand a class called Smilly. This class is detailing the behavior of a computer sprite in the award winning (in my mind) game Space Smilies. You're trying to figure out how this is implemented. The class is many lines long so it's obviously doing something something fancy. You notice that it inherits from a class called EvilThing. EvilThing extends from ComplexComputerSprite. This class extends ComputerSprite which extends AbstractSprite. All member variables are accessible from all subclasses. Where to start?

Making a member variable protected means it's accessible to all sub-classes. Any one of these sub classes can read an write to them. You need to understand how the class and all the parent classes interact to create the desired behavior. Just as making a member as public is bad practice because to understand who is modifying that public variable you need to track down every piece of code that uses it, using protected members in a complex hierarchy is bad for the same reasons.

This also applies to protected, non-final protected and public methods but it worse from the point of view of modifying the parent class. In order to understand if modifying a public or protected method will accomplish the desired behavior you need to understand the entire object hierarchy because there's no telling if one of the sub-classes has over-ridden any one of the public or protected methods and many make assumptions about how the class operate. If you stomp on one of its assumptions you'll cause a bug.

Understanding the entire class hierarchy and how each class works with its parent is labor intensive to say the least.

Because any method can be over-ridden anywhere in the hierarchy and because any state variable can be used by any subclass it means you have to be meticulous about checking every class in the hierarchy for unexpected overrides. With inheritance it's very easy to create a hierarchy that's so interconnected that you need to understand how everything works before you can understand any part of it. You might have an object composed of many classes but the classes over-ride each other's behavior in such a way that it effectively creates one big coupled class out of many.

Composition, on the other hand, makes it easier to create and maintain encapsulation and lower coupling. It's easier to control which members and methods are accessed and there's no invisible over-riding of behavior to trip you up. Methods that are public make up the object's external interface. Simple as that. (Assuming the normal best practice of trying to hide your member variables and implementation inside by making them private).

Composition is not as powerful as inheritance, however, so you might not be able easily do what you want using only composition. Which goes back to our original question, what do you do if inheritance is the right tool for the job? What if you need the power?

Java has a few tools to help you out in the form of the keywords abstract, protected, private and final.

If you find yourself with a sprawling inheritance hierarchy you can help out yourself and any future maintainer by:


  1. Reducing scope: If it can be made private then make it private. If it can't be made private then make it protected.
  2. Adding final: If a method can be made final then make it final. It's one less method that can have sneaky override or one less variable that can be modified.
  3. Make methods that *should* be overridden abstract: In complex objects it's more explicit to have all your overridable methods marked abstract so that's it's clear that subclasses need to implement these.
  4. Don't make deep inheritance hierarchies. The rule I use is a class's true size includes all it's parent classes all the way up to java/lang.Object.

to be continued...

No comments: