DEV Community

Cover image for Composition over Inheritance — it’s not always one or the other!
Cory Meikle
Cory Meikle

Posted on

Composition over Inheritance — it’s not always one or the other!

You may have heard "Prefer composition over inheritance" shouted about a lot in software design, it's something that's been accepted for a very long time. Inheritance has a bad name due to how easy it is to end up with a bad, fragile hierarchy.

Instead of the typical discussion of this topic using Dog/Cat/Animal classes, let's use a real world example, using a payment gateway.

Inheritance for Consistency

Before using inheritance, you must ask yourself if you ever see the core logic having to behave in a different way. Now, in our case from the payment gateway integration, we will always be calling our payment provider via HTTP. Meaning there will always be Headers present, and always an Endpoint.


The above diagram shows AbstractPaymentRequest at the top, enforcing consistency across all requests. The child classes only need to worry about implementing the endpoint they call, and the payload they need to send.

In our example (Actually using Opayo) the headers we need to supply on all of our requests are Authentication (Basic auth) and typically the Content-Type. So, this is a job for the Abstract method.

Once more, the send() method is the concrete implementation of building the request object and sending the request to the Opayo server. This returns to us a PaymentResponse class (Not implemented in the diagram, just think of it as a value class that parses the response we retrieve from the API).

This is also referred to as the Template Method Pattern.

What does this achieve?

What we've achieved here is the knowledge that any class that extends our AbstractPaymentRequest will be sent via HTTP, and all of the authentication will be handled by its parent. In addition to this, it also means that to call a new endpoint on Opayo we simply need to create a new class that extends the AbstractPaymentRequest. This means we also adhere to the Open-Closed Principle.

Where does composition come into this?

If we look at the Opayo API documentation, primarily under Transactions, there's a transaction type of "Payment". You don't need to look at all of the expected payload, but specifically the "paymentMethod" object it expects.

It can take in different types of payment methods. Card, paypal, applePay, googlePay, etc.

It would be pretty bad if we had logic within our CreatePayment class to change the paymentMethod object based on our input. It's a bit of a code smell, and it also means we'd have to update the class every time we add a new payment method, meaning if we one day added google pay we'd need to do regression testing.

This is where composition comes in to assist us.

<?php

class PaymentRequest extends AbstractPaymentRequest
{
    public function __construct(
        protected PaymentMethodBuilder $paymentMethodBuilder
    )
    {}

    public function getEndpoint(): string
    {
        return '/api/v1/transactions';
    }

    public function getPayload(): array
    {
        return [
            // ...
            'paymentMethod' => $this->paymentMethodBuilder->build(),
            // rest left off for ease of reading
        ];
    }
}

interface PaymentMethodBuilder
{
    public function build(): array;
}

class CardPaymentBuilder implements PaymentMethodBuilder
{
    public function build(): array
    {
        // implementation of payload for card payment
        return [];
    }
}

class GooglePayBuilder implements PaymentMethodBuilder
{
    public function build(): array
    {
        // implementation of payload for google pay payment
        return [];
    }
}
Enter fullscreen mode Exit fullscreen mode

As you can see above, we have an interface PaymentMethodBuilder and currently 2 classes, CardPaymentBuilder and GooglePayBuilder, both of these classes are simply responsible for building the payload that Opayo expects of their respective methods.

Within the CreatePayment class we can see it takes in the PaymentMethodBuilder on construction, which means we can easily swap the payment method we use based on what the user has selected on the frontend, all without bloating our request class with logic of swapping, and sticking to the Open-Closed principle.

Closing

The point here isn't that inheritance is bad or that composition is always better. It's that they serve different purposes, and it's not always one over the other. I hope I've made that clear here.

Top comments (0)