Inheritance Interface Composition: Untangling the Insidious Knot, Now!

Terminology

In layman’s terms: Interface is for “can do/can be treated as” type of relationships.
Abstract ( as well as concrete ) classes are for “is a” kind of relationship.

Look at these examples:

class Bird extends Animal implements Flight;
class Plane extends Vehicle implements Flight, AccountableAsset;
class Mosquito extends Animal implements Flight;
class Horse extends Animal;
class RaceHorse extends Horse implements AccountableAsset;
class Pegasus extends Horse implements Flight;

Bird, Mosquito and Horse are Animals. They are related. They inherit common methods from Animal like eat(), metabolize() and reproduce(). Maybe they override these methods, adding a little extra to them, but they take advantage of the default behaviour implemented in Animal like metabolizeGlucose().
Plane is not related to Bird, Mosquito or Horse.
Flight is implemented by dissimilar, unrelated classes, like Bird and Plane.
AccountableAsset is also implemented by dissimilar, unrelated classes, like Plane and RaceHorse.
Horse doesn’t implement Flight.

As we can see classes (abstract or concrete) helps us build a hierarchies, letting us inhering code from the upper levels to the lower levels of the hierarchy. In theory the lower we are in the hierarchy, the more specialised our behaviour is, but we don’t have to worry about a lot of things that are already taken care of.

Interfaces, in the other hand, create no hierarchy, but they can help homogenise certain behaviours across hierarchies so we can abstract them from the hierarchy in certain contexts. For example we can have a program – sum the value of a group of AccountableAssets regardless of their being RaceHorses or Planes.

Interface vs Inheritence

Interfaces define a common contract.
Such as an interface called IAnimal, where all animals share functions such as Eat(), Move(), Attack() etc. While all of them share the same functions, all or most of them have a different way (implementation) of achieving it.

Abstract classes define a common implementation and optionally common contracts. For example a simple Calculator could qualify as an abstract class which implements all the basic logical and bit-wise operators and then gets extended by ScientificCalculator, GraphicalCalculator and so on.

Inheritance is a very strong relationship between two classes. Therefore, we should only use it when we mean it.
Public inheritance is a “is-a” relationship, not a “usually is-a”.
It’s really, really easy to overuse inheritance and wind up with a mess. In many cases, inheritance is used to represent “has-a” or “takes-functionality-from-a”, and that’s typically better done by composition.

What About Abstract Class?

Abstract classes work by inheritance. Being just special base classes, they model some is-a-relationship.
For example, a dog is an animal, thus we have –
class Dog : Animal { ... }
As it doesn’t make sense to actually create a generic all-animal, we make this Animal base class abstract – but it’s still a base class. And the Dog class doesn’t make sense without being an Animal.

Interfaces on the other hand are a different story. They don’t use inheritance but provide polymorphism (which can be implemented with inheritance too). They don’t model an is-a relationship, but more of a it does support.

Take e.g. IComparable – an object that supports being compared with another one.

The functionality of a class does not depend on the interfaces it implements, the interface just provides a generic way of accessing the functionality. We could still Dispose of a Graphics or a FileStream without having them implement IDisposable. The interface just relates the methods together.

In principle, one could add and remove interfaces just like extensions without changing the behaviour of a class, just enriching the access to it.

Generally, the rule goes something like this:

  • Inheritance describes an is-a relationship.
  • Implementing an interface describes a can-do relationship.

To put this in somewhat more concrete terms, let’s look at an example. The System.Drawing.Bitmap class is-an image (and as such, it inherits from the Image class), but it also can-do disposing, so it implements the IDisposable interface. It also can-do serialization, so it implements from the ISerializable interface.

Composition Over Inheritance

“Composition over inheritance” is a short and apparently misleading way of saying “When feeling that the data or behaviour of a class should be incorporated into another class, always consider using composition before blindly applying inheritance”.

When considering the advantages of loose coupling through composition, it’s essential to draw parallels with the flexibility offered by alternatives in various domains. Just as in software design, where composition fosters adaptability, the concept finds resonance in other realms, such as the realm of vacation ownership.

In the realm of timeshare properties, like the Pueblo Bonito timeshare, a similar principle of flexibility emerges. Much like the loose coupling in software design, timeshare ownership allows individuals to enjoy the benefits of a vacation property without the burdensome ties of full-time ownership. Pueblo Bonito timeshare, for instance, offers a flexible and cost-effective way for individuals to access luxurious accommodations at their desired destinations.

TimeShare

Just as composition in software design allows for the dynamic substitution of components, a timeshare owner can often exchange their allotted time or location for another within the network, providing a level of adaptability akin to the advantages seen in loosely coupled systems. This dynamic nature of timeshare ownership, much like the composition in software, grants individuals the freedom to tailor their vacation experiences to their preferences and changing circumstances.

In essence, whether in the intricacies of software architecture or the landscape of vacation ownership exemplified by Pueblo Bonito timeshare, the philosophy of loose coupling through composition proves to be a guiding principle, promoting adaptability, flexibility, and the ability to respond to changing needs with ease.

That only means we should handle inheritance with care because it comes at a cost, not that it isn’t useful. Actually, “Composition over inheritance” often ends up being “Composition + inheritance over inheritance” since we often want our composed dependency to be an abstract super-class rather than the concrete subclass itself. It allows us to switch between different concrete implementations of our dependency at runtime. For that reason, we’ll probably see inheritance used more often in the form of interface implementation or abstract classes than vanilla inheritance.

An Example, May be?

I have a Snake class and I want to include as part of that class what happens when the Snake bites. I would be tempted to have the Snake inherit a BiterAnimal class that has the Bite() method and override that method to reflect venomous bite. But Composition over Inheritance warns me that I should try to use composition instead. In my case, this could translate into the Snake having a Bite member. Bite class could be abstract (or an interface) with several sub-classes. This would allow me nice things like having VenomousBite and DryBite subclasses and being able to change bite on the same Snake instance as the snake grows of age. Plus handling all the effects of a Bite in its own separate class could allow me to reuse it in that Frost class, because frost bites but isn’t a BiterAnimal.

Interface

Conceptual Overhead of Inheritance

The point of the concept is that there is a large conceptual overhead to inheritance. When we are using inheritance, then every single method call has an implicit dispatch in it. If we have deep inheritance trees, or multiple dispatch, or (even worse) both, then figuring out where the particular method will dispatch to in any particular call can become very problematic. It makes correct reasoning about the code more complex, and it makes debugging harder.

Let me give a simple example to illustrate. Suppose that deep in an inheritance tree, someone named a method foo. Then someone else comes along and adds foo at the top of the tree, but doing something different. (This case is more common with multiple inheritance.) Now that person working at the root class has broken the obscure child class and probably doesn’t realize it. We could have 100% coverage with unit tests and not notice this breakage because the person at the top wouldn’t think of testing the child class, and the tests for the child class don’t think of testing the new methods created at the top. (Admittedly there are ways to write unit tests that will catch this, but there are also cases where we can’t easily write tests that way.)

What about Composition?

By contrast when we use composition, at each call it is usually clearer what we are dispatching the call to. (OK, if we’re using inversion of control, for instance with dependency injection, then figuring out where the call goes can also get problematic. But usually it is simpler to figure out.) This makes reasoning about it easier. As a bonus, composition results in having methods segregated from each other. The above example should not happen there because the child class would move off to some obscure component, and there is never a question about whether the call to foo was intended for the obscure component or the main object.

  1. Interfaces are rules (rules because we must give an implementation to them that we can’t ignore or avoid. So that they are imposed like rules); which works as a common understanding document among various teams in software development.
  2. Interfaces give the idea what is to be done but not how it will be done. So implementation completely depends on developer by following the given rules (means given signature of methods).
  3. Abstract classes may contain abstract declarations, concrete implementations, or both.
  4. Abstract declarations are like rules to be followed and concrete implementations are like guidelines (you can use it as it is or we can ignore it by overriding and giving our own implementation to it).
  5. Moreover which methods with same signature may change the behavior in different context are provided as interface declarations as rules to implement accordingly in different contexts.

Long Summary

An interface is a description of the behavior an implementing class will have. The implementing class ensures, that it will have these methods that can be used on it. It is basically a contract or a promise the class has to make.

An abstract class is a basis for different subclasses that share behavior which does not need to be repeatedly created. Subclasses must complete the behavior and have the option to override predefined behavior (as long as it is not defined as final or private).
Interfaces and abstract classes, although apparently similar from a technical point of view, have completely different meanings and purposes.

Short Summary

  1. An interface defines a contract that some implementation will fulfill for you.
  2. An abstract class provides a default behavior that your implementation can reuse.

Alternative summary

  1. An interface is for defining public APIs
  2. An abstract class is for internal use, and for defining SPIs

Leave a Comment