Inheritance in OOP is Evil

Inheritance is a fundamental concept in object-oriented programming (OOP). It allows creations of a new class based on an existing class, promoting polymorphism and code reuse. However, while inheritance can be powerful, it is also almost always wrong. Let’s discuss the pitfalls of inheritance with some simple example.

What is Inheritance?

Inheritance allows a class (called a subclass or child class) to inherit fields and methods from another class (called a superclass or parent class). The subclass can then extend or override the inherited methods to provide specific functionality. This concept is often described using an “is-a” relationship; for example, a Pigeon is-a Bird. Bird can walk and fly. Pigeon is a bird and there for can walk and fly. Intuitive and straight forward.

open class Bird {
    fun walk() {
        // Implement walking behavior
    }

    fun fly() {
        // Implement flying behavior
    }
}

class Pigeon : Bird

When Inheritance Breaks : The Penguin Problem

Inheritance works well when your assumptions about the relationships between classes hold true. However, it can break down when those assumptions are incorrect. We have previously established an assumption that birds can walk and fly. This was true for our only concrete class: Pigeon. Let’s take the example of adding a Penguin class to our hierarchy. Penguins are birds, but they cannot fly. Our original assumption that all birds can walk and fly is no longer true. We’ve made a wrong abstraction. One way of fixing this is to introduce a new layer in type hierarchy.

open class Bird {
    fun walk() {
        // Implement walking behavior
    }
}

open class FlyableBird : Bird {
    fun fly() {
        // Implement flying behavior
    }
}

class Pigeon : FlyableBird

class Penguin : Bird

Now, the Bird class only has walk() method, and the FlyableBird class extends Bird by adding a fly() method. This restructuring allows us to represent without violating the “is-a” relationship.

Just like the penguin problem we’re dealing with, changes in requirements cause shifts in our assumptions, leading to changes in abstraction. This becomes even more challenging when abstractions are interconnected and dependent on each other, as with deep layers of type hierarchies. The more layers of abstraction there are, the higher the risk of breaking those abstractions.

When Inheritance Breaks No. 2 : Client – Superclass Coupling

Let’s extend our example by adding an Ostrich class. An ostrich, like a penguin, can walk but cannot fly, so it should naturally inherit from the Bird class.

open class Bird {
    fun walk() {
        // Implement walking behavior
    }
}

open class FlyableBird : Bird {
    fun fly() {
        // Implement flying behavior
    }
}

class Pigeon : FlyableBird

class Penguin : Bird

class Ostrich : Bird

Now, imagine a new requirement: we need to add a swimming capability to our birds.

open class Bird {
    fun walk() {
        // Implement walking behavior
    }

    fun swim() {
        // Implement swimming behavior
    }
}

open class FlyableBird : Bird {
    fun fly() {
        // Implement flying behavior
    }
}

class Pigeon : FlyableBird

class Penguin : Bird

class Ostrich : Bird

However, ostriches generally can’t swim. Yet, because the swim() method is part of the Bird class, it becomes available to all subclasses, including Ostrich. This results in client code being tightly coupled to the superclass’s methods, regardless of whether those methods make sense for all subclasses.

fun main () {
    val ostrich = Ostrich()
    ostrich.swim()
}

This coupling between client code and the superclass’s methods can lead to problems. In this case, to prevent ostriches from having a swim() method, we’d need to refactor our inheritance hierarchy once again. Without this change, our design inadvertently allows actions that don’t align with the actual behavior of certain subclasses, illustrating how fragile inheritance hierarchy can be once again.

Composition over Inheritance

The principle of “favor composition over inheritance” is widely recommended in software design because it addresses many of the issues that inheritance can introduce. Composition is more adaptable to changing requirements, making your codebase more resilient and easier to maintain. By using composition, you avoid creating fragile hierarchies that are prone to breaking when requirements evolve.

Here’s an example that illustrates how composition can replace inheritance:

class Walker {
    fun walk() {
        // Implement walking behavior
    }
}

class Flyer {
    fun fly() {
        // Implement flying behavior
    }
}

class Swimmer {
    fun swim() {
        // Implement swimming behavior
    }
}


class Pigeon(
    private val walker: Walker, 
    private val flyer: Flyer,
    private val swimmer: Swimmer,
) {
    fun walk() = walker.walk()
    fun fly() = flyer.fly()
    fun swim() = swimmer.swim()
}

class Penguin(
    private val walker: Walker, 
    private val swimmer: Swimmer,
) {
    fun walk() = walker.walk()
    fun swim() = swimmer.swim()
}

class Ostrich(
    private val walker: Walker, 
) {
    fun walk() = walker.walk()
}

Although the concrete bird classes in this example are still coupled to specific behavior classes, this coupling is encapsulated within the bird classes themselves. The client code interacting with these classes doesn’t need to know or care about the underlying components. This hidden complexity makes the code easier to use and maintain.

Conclusion

In this blog post, we explored how inheritance often leads to fragile type hierarchies and tight coupling between subclasses, superclasses, and client code, making the code prone to breaking when requirements change. Composition offers a more resilient alternative by allowing you to build classes from smaller, flexible components. This approach avoids rigid hierarchies, reduces the risk of breaking changes, and keeps dependencies encapsulated within classes, making the code more adaptable and maintainable.

Leave a Reply

Your email address will not be published. Required fields are marked *