Any book on OOP must explain inheritance, simply because it’s an important part of OOP in many languages. And since it’s so hard to explain and understand a large part of the book will be just about inheritance. This may lead to the false impression that OOP is just about inheritance.
Inheritance it’s not something you’d actually use that often. It is sometimes handy and can result in better code. But too much inheritance actually often causes many problems.
Some languages don’t even support inheritance of state. Google Go is one example. It still has inheritance of type, but a class can only inherit the type of an interface. It can’t inherit anything from other classes. That’s exactly where most of the problems come from.
Problems with type inheritance
Often the type hierarchy is just plain wrong. And even if it works in a project it can often be unclear to some programmers. Is a square a special kind of a rectangle? One that has both sides of the same length? Or is a rectangle an extended version of the square because it can have two different side lengths? There’s no right answer, so it would be better to just have two separate types and one truly general interface for any shape and maybe one for polygons.
Inheritance is static
You inherit at compile-time, not at runtime. Method binding is dynamic but that doesn’t make your design more dynamic. Any alternative that allows to “inherit” something at runtime has a great advantage. It is dynamic and therefore more flexible.
Fragile Base Class Problem
Whenever you implement a method as non-final you take the risk that someone overrides it. That might be OK if you clearly specify everything about that method (i.e. with very precise javadoc). But your class becomes fragile because anyone could extend your class and override such methods ignoring your specifications.
To prevent this you could simply introduce some coding guidelines that can prevent such a problem. The main task of a compiler is to translate source code to machine code (i.e. byte code for the JVM). A modern compiler also checks some things like type safety, dead code and other things. But more complex analysis must be done by other tools (CheckStyle, SpotBugs, PMD etc.). CheckStyle already knows a check named Design For Extension. It will “protect superclasses against being broken by subclasses“.
You always inherit everything. With inheritance you can get types with many layers of implementation. And that is hard to maintain. Changes on a base class may lead to unexpected results in all of it’s subclasses. Keeping all fields private and all implemented methods final helps, but this just adds more guidelines that need to be followed.
With multiple inheritance of state there is quite a risk of inheriting something twice. In Java there’s only single inheritance of state but you have multiple inheritance of type and behaviour. There’s no problem if a type (interface) is inherited twice (with the same generic type arguments). The compiler will tell you if inheritance of a default method is ambiguous. So all this isn’t really a problem but also adds to the complexity.
There are some things you should always consider first, so you can keep your code clean and robust.
Composition Over Inheritance
Instead of inheriting another type you can simply use it internally. There’s a Wikipedia article on this, the “Master and Student…” discussion in Head First Design Patterns explains this too, and then there’s Item 16 in Effective Java. You can read both on Google Books.
- Wikipedia > Composition over inheritance
- O’Reilly > Head First > Design Patterns > The Decorator Pattern
- Joshua Bloch > Effective Java > Item 16
Template Method Pattern
This pattern also helps with complex type structures. To have all methods final, abstract or empty you can define one public method in the base class and have it call abstract or empty methods. The subclasses then can implement those methods.
- Wikipedia > Template method pattern
- O’Reilly > Head First > Design Patterns > The Template Method Pattern
Non-static methods are always dynamic in Java. The runtime will chose the most specific method. You can simply prevent this by making the method final. It can’t be overridden. To prevent dynamic binding completely you can make it static.
If you ever used Swing then you probably know the class SwingUtilities. It’s “[a] collection of utility methods for Swing.” Such a class consists only of public static methods. Those methods usually accept parameters of very basic types (usually defined as interfaces). Other examples are Collections, Arrays, Files and other classes that have an “s” at the end.
Those methods could just as well be declared in the actual type but separation simply helps keeping a clear division of static and non-static methods. In Java 8 you can put static methods into interfaces.
However, you end up in lots of method calls like
Foo.something(foo) instead of just
Inheritance is hard to learn and fully understand. I recommend to use it as little as possible. But then how can you learn to use it properly? You will probably learn it every time you use inheritance over composition just to end up with a way too complex type hierarchy.
But there are good examples of inheritance. I think EnumSet is a proper use of inheritance. It’s abstract and the actual implementations are not public. One base class and two implementations. Nothing too complex. I just don’t understand why they didn’t create an interface so that we could implement our own EnumSets without the given base class. But then there’s already
java.util.Set as an abstraction of the set data type.
With an interface, a base class and a few implementations you can have proper design based on inheritance without too much complexity. You can find an example of this in my project EnumBitSet.