OOP Abuse of Inheritance

Summary

A discussion of inheritance in OOP, of the intents and the typical misuse of it.

Intent of Inheritance

The intent of inheritance in OOP is to allow common functions and member variables to be shared when two child classes are subtypes of a parent class. Subtype means it is a refinement of the parent class, not a separate entity. For example

class Animal:
    def __init__(self):
        self._has_hair = None
        self._heart_chambers = 0
        self._produce_milk = False


class Mammal(Animal):
    def __init__(self):
        super().__init__()
        self._has_hair = True
        self._heart_chambers = 4
        self._produce_milk = True


class Reptile(Animal):
    def __init__(self):
        super().__init__()
        self._has_hair = False
        self._heart_chambers = 3

Both Mammals and Reptiles are animals and are similar. But Mammals produce milk while Reptiles don't.

A main feature of inheritance is the ability to pass an instance of a child as its parent. To ensure that's possible the Liskov Substitution Principle must be followed or subtle bugs are will occur.

https://en.wikipedia.org/wiki/Liskov_substitution_principle

For example, if there is some code that works with any animal, then it must be possible to pass a Mammal or a Reptile instance to that code without any failures. In general that means that the calling code ONLY uses the functions and member variables of the parent class.

Misuse of Inheritance

Inheritance allows any code in the parent class to be shared among all child classes. This a great convenience but leads to misuse.

For example, one real world app was made of 3 classes. Two were legit, but the third class was called Generic. Both of the child classes inherited from Generic and so could use any function in there. In other words, the Generic functions were globals.

Eventually another class was added and needed a couple of functions in Generic, so it became a parent class for it as well.

Each instance of the child classes had its own instance of Generic. That means that common member variables in Generic were not shared between the child classes. That led to workarounds and finally to the creation of a Singleton that Generic would use and thereby get around a "limitation" of inheritance.

Over time, Generic had nearly a thousand functions in it. Why? Because it was very difficult to tell if a function already existed, so developers just created new functions. And so common functions existed and were named with similar names... or not. Reading the calling code was difficult since similar named functions behaved differently and differently named functions behaved similarly.

When a bug was found in one function, it was fixed there but not in the other common similar functions, usually because no-one knew where they were. That led to odd bugs where "the issue was fixed" but still occurring. Why? Because one caller used one of the common functions and another caller used another common similar function.

In short, Generic was a dumping ground used for convenience.

How to recover

Refactor, over and over again.

  • convert Generic (to use the example above), to an independent class and have a singleton instance of it. This means that any member variables are shared across all classes. And use this opportunity to remove this as a parent from Generic.
  • reshuffle the layout of the functions inside Generic. Start to cluster common behaving functions into an area of the file. This will make finding common similar functions easier to do. (Note: when we did this, we found nearly identical functions but with different names).
  • refactor common similar functions into smaller functions. Look through as many existing functions as possible to replace common code with these new, smaller functions. This will clearly demarcate the differences between functions.
  • track these smaller functions and see if there is a potential for creating a class to encapsulate their behaviors.
  • determine if a function is strictly used by only one child class and not any others. Move that function to the child class.
  • extract out as many classes as possible from Generic to break down the code into smaller pieces. The callers will then use local instances of these new classes, or they can be made shared singletons as well.

- John Arrizza