Composition vs. inheritance in PHP

Inheritance. One of the four fundamental concepts in object oriented programming, by the book. Inheritance, encapsulation, polymorphism and abstraction. But, as you may have noticed, this concept is becoming increasingly unpopular as a useful concept in modern programming, particularly when it comes to maintaining large code bases for projects that last for years and have been worked on by a variety of developers and teams. Such projects may be messy by definition, but inheritance can be one of the biggest headaches at times.

Is it, however, due to the concept itself? Is it possible that one of the most important and fundamental concepts in object oriented programming has become completely obsolete? Not necessarily, I would argue. Typically, concepts, design patterns, architecture types, programming paradigms, and programming languages are not the specific problems. Maybe I’ll come across as cliche, but they’re just tools. Usually, we who use these tools make a mess of things by using them incorrectly, not fully understanding what they are useful for, wanting to try a newly learned concept everywhere for practice purposes, and overusing them.

What is inheritance?

Let’s first see what is inheritance. I’ll use some code examples written in PHP, but the concept is basically the same or very similar in many other programming languages like TypeScript or pure JavaScript, etc.

class Visit
{
    public function setInActiveVisit()
    {
        echo 'The user is currently in an active visit';
    }
}

class Patient extends Visit
{
    public function callTheDoctor()
    {
        // Do any necessary logic
        ...

        // Set the user in active visit
        $this->setInActiveVisit(); 
    }
}

class Doctor extends Visit
{
    public function acceptPatientCall()
    {
        // Do any necessary logic
        ...

        // Set the user in active visit
        $this->setInActiveVisit(); 
    }
}

$patient = new Patient();
$patient->callTheDoctor();

$doctor = new Doctor();
$doctor->acceptPatientCall();

The preceding example shows a Visit superclass with a method setInActiveVisit that is in charge of setting the user status to indicate that it is currently in an active visit. Furthermore, this class has two different inheritances in the Patient and Doctor classes. These two classes place the user in an active visit on two different occasions: when the patient calls the doctor and when the doctor accepts the patient’s call.

What’s the problem with inheritance?

What is the potential issue here? The Visit class is inextricably linked to both the Patient and Doctor classes. The Visit class has no idea what these two classes are doing because it knows nothing about them, so if this Visit class is changed, especially the setInActiveVisit method, it is very easy to break the functionality of every class that might inherit it and use this specific method. Using this example, you can probably guess that this type of superclass, called Visit, contains a lot of stuff that isn’t always related to all types of users. For example, we may have Admin users who are never included in online visits, such as doctors and patients, but the Visit class may contain methods that are applicable to all types of users. And things could get even more complicated if we have a class that inherits a class that inherits a class that inherits another class that also inherits a class. One critical method change, and we’re likely to be in a difficult maintenance situation. This is why the inheritance concept is so heavily criticized today. Because it is sometimes overused.

What is composition and how to use it?

Let’s look at composition and how we can use it to solve this problem. This is a design concept that is used to describe one object that contains another. Composition implies a strong sense of ownership. When one object owns and manages the lifecycle of another object, all children are destroyed as well. Using simple dependency injection, we can achieve decoupling of the business logic or domain of action. However, these are simply big and strong words, and the actual implementation appears to be quite simple. Allow me to demonstrate.

class Visit
{    
    public function setInActiveVisit()
    {
        echo 'The user is currently in an active visit';
    }    
}

class Patient
{
    public function __construct(public readonly Visit $visit)
    {}

    public function callTheDoctor()
    {
        // Do any necessary logic
        ...

        // Set the user in active visit
        $this->visit->setInActiveVisit();    
    }
}

class Doctor
{
    public function __construct(public readonly Visit $visit)
    {}

    public function acceptPatientCall()
    {
        // Do any necessary logic
        ...

        // Set the user in active visit
        $this->visit->setInActiveVisit();    
    }
}

$visit = new Visit();
$patient = new Patient($visit);
$patient->callTheDoctor();

$visit = new Visit();
$patient = new Doctor($visit);
$patient->acceptPatientCall();

We have completely replaced inheritance with composition! The final few lines of code demonstrate a completely decoupled implementation that is easily changed. The implementation classes, such as Visit, Patient, and Doctor, can be easily replaced by changing the implementation without changing anything in those classes, ensuring that nothing breaks. Also, keep in mind that dependency injection can be done even better with interfaces, so make this Visit class implement a specific interface to improve decoupling and make this code base even easier to test and maintain.

When is OK to use inheritance?

So, when might it be a good idea to use inheritance?

  • You are performing higher level domain modeling. When you need to write code that is still related to a specific domain but is from a higher purpose, implying that no specific business logic is included. When working on a specific domain logic or business logic that may be changed in the future, you should absolutely avoid using inheritance.
  • You are creating a framework, a framework extension, or something related to the design’s infrastructure. Developing such things is usually not tightly related to a specific domain, or it should not be, in which case feel free to use some inheritance. You’re creating a custom package or driver to help with database queries. Because you are unlikely to (or should not) have any specific business logic that your client will change as the business changes in the future, using inheritance in your code design may be a good solution for some parts.
  • You’re doing differential programming. In other words, you need a helper or utility class that is nearly identical to another helper class you already have, but you need some extra functionality or method rewrites and you don’t want to build the helper class from scratch. But proceed with caution. Sometimes treating helper and utility classes like this is the quickest way to hell. 🙂