Recently I had an interesting discussion on the subject I provoked in a post on the official "PHP Srbija” User Group Facebook page. Literal translation of the original post:

If I ever decide to be a teacher, first thing I'll try to explain is that inheritance is not and shouldn't be considered as one of the pillars of object-oriented programming.

I immediately got negative feedback due to this too incomplete and perhaps provocative statement, so I tried to clarify my point by adding:

shouldn't be considered as one of the pillars != abolish. Inheritance is an essential OOP feature rather than a pillar, but should be used carefully and moderately.

So my point is clear - inheritance is an indispensable feature of the object-oriented programming, but it is often being abused by developers (both experienced and less-experienced), because of the poor way inheritance paradigm was described and presented to them.

Likewise, inheritance has many disadvantages in comparison to some other alternative techniques that will be explained in this article, and thereby should be used as a last resort. Examples will be written in PHP, but it should be easy to understand if you are not familiar with the language.

To inherit or not to inherit?

To start, when you're tempted to extend some class, ask yourself these simple questions:

  • Will the new class be of the same type as parent class?
  • Will the new class be at the same level of abstraction as a parent class?
  • Should the new class have the same responsibilities as a parent class?
  • Should public API of the new class contain methods from the parent class?

If the answer to any of these questions is no, then inheritance is not the proper solution.

Decorate

There's a specific case in which all of the conditions from the questionnaire above are fulfilled, whereas override of some method is needed in order to add or alter some behavior of the parent class. Let me demonstrate this with a practical example. Examples are written in PHP, but you should have no problem understanding them even if you're not familiar with the PHP language.

Say we have a UserRepository component for retrieving users in our application:

interface UserRepositoryInterface
{
    public function find(string $id) : User;

    public function findAll() : UserCollection;
}
class UserRepository implements UserRepositoryInterface
{
    // some implementation...
}

Now say we want to extend repository with a caching functionality, so that find() and findAll() results can be cached. How do we do that? Refactor existing UserRepository implementation? Create a sub-class? No, decorate!

class CachingUserRepository implements UserRepositoryInterface
{
    protected $userRepo;

    public function __construct(UserRepositoryInterface $userRepo)
    {
        $this->userRepo = $userRepo;
    }

    public function find(string $id) : User
    {
        $cache = $this->getCache();

        if ($cache->has($id)) {
            return $cache->get($id);
        }

        $user = $this->userRepo->find($id);

        $cache->set($id, $user);

        return $user;
    }

    //...
}

Bottom line is that decorators allow you to dynamically add or alter functionality of an existing class. Whenever you are tempted to override some behavior in order to add new functionality, apply composition.

Black Box programming

By favoring composition over inheritance, like in the above example, we're shifting from so called White Box programming towards Black Box programming. In case of White Box programming, we have to be aware of the parent class implementation and watch for potential internal changes that author of the class can introduce. In contrast, with Black Box programming we can be completely ignorant of the implementation and concerned only with the public interface of a class we are using.

By extending a class, you are taking the risk of potential breaks that author of the parent class can produce. You are right when you say that the public API of a base class can break the same way as its internals, but you'll agree that those changes happen a lot less frequently. Also, you really can't blame the author for changing internal properties of his class. Question you can ask is why that particular class hasn't been marked as final.

Valid use cases for inheritance

There is an opinion that states that inheritance should only be performed in case of abstract classes. And of course, inheritance is inevitable in that particular case. How to properly design abstract classes is a story of its own and beyond the scope of this article.

But also, in the questionnaire from the beginning of this article I mentioned a crucial term – type. Classes that we write every day can generally be divided into two categories: those that represent types and those that represent behavior. Typical examples for types are entities and value objects, while services, repositories, mappers, loggers, DBALs and similar are examples of classes that define certain behavior.

In conjunction with the subject of this article, generally speaking inheritance does make sense when designing classes that represent types, but it doesn't in case of classes representing behavior. I won't go further with this one, because Anthony Ferrara wrote an excellent, detailed explanation in his Beyond Inheritance article.

Summary

  • Composition is loose-coupling, while inheritance results in static, hard-coupling
  • Decorator pattern allows us to dynamically add or alter functionality of an existing class
  • By favoring composition over inheritance, we're shifting from White Box programming towards Black Box programming
  • Inheritance does make sense when designing classes that represent types, but it doesn't in case of classes representing behavior

Exhausted topic

One way or another, this story has been told many times. Just search for "favor composition over inheritance" phrase and you'll see what I mean by that. The problem is that that message does not reach newbie developers. In my opinion, that is happening due to several factors:

  • obsolete and outdated resources (books, online tutorials, articles)
  • ignorance of people sharing knowledge (teachers, lecturers) in new, modern principles and their stubbornness regarding old postulates
  • unwillingness of such people to refresh their knowledge and hostile attitude towards everything they are not familiar with
  • lack of ambition among their audience (students, learners) to think out of the box and beyond what they've learned in school

To have fewer articles like this one in the future, the above items have to be tackled somehow. Majority will eventually be resolved over time, with never, hopefully better generation of programming teachers. Rest is solely developers' responsibility and they passion towards improving their development skills.