Skip to content
Logo Theodo

Writing Quality Code Using the Chain of Responsibility Pattern

Moad Fethallah6 min read

Metal Chain

At the beginning of my software engineering internship, I had little experience with writing clean code. The features were functional but there was hardly any underlying organization. I was lacking guidelines on what classes to create and how they should interact with each other. That’s when my mentor introduced me to design patterns. These are solutions for creating flexible and maintainable modules.

In the upcoming sections, I will showcase the use of the Chain of Responsibility pattern and how it helped us write scalable and easily testable code on a production-level API.

A design problem

On a project for a renewable-energy provider, I was working on a consumption monitoring app. One goal was to retrieve the user’s activities over a time period. An activity is a carbon footprint calculated based on energy consumption. To compute the activities, we had to fetch and process the consumption in the following priority order:

Data fetching and processing flow diagram

The returned activities are the result of the first of these methods that returns a non-empty array.

A trivial solution

While designing a solution for the presented situation, we have to take into account the maintainability of the code, the simplicity of adding and removing data sources as well as testing our service. An initial solution may consist of cramming everything in one class.

namespace Component\Activity\Provider;

class ActivityProvider {

    public function retrieveActivities(\DateTime $startDate, \DateTime $endDate): array
    {
        if (count(retrieveMeterActivities($startDate, $endDate)) > 0) 
            return retrieveMeterActivities($startDate, $endDate);

        if (count(retrieveEstimatedActivities($startDate, $endDate)) > 0) 
            return retrieveEstimatedActivities($startDate, $endDate);

        if (count(retrieveMacroActivities($startDate, $endDate)) > 0) 
            return retrieveMacroActivities($startDate, $endDate);

        return [];
    }
    
    private function retrieveMeterActivities(\DateTime $startDate, \DateTime $endDate)
    {
        $data = $this->fetchMeterData($startDate, $endDate);
        return $this->processMeterData($data);
    }

    private function fetchMeterData(\DateTime $startDate, \DateTime $endDate)
    {
        /* Fetch data */
    }

    private function processMeterData(array $data)
    {
        /* Process raw meter data */ 
    }

    private function retrieveEstimatedActivities(\DateTime $startDate, \DateTime $endDate)
    {
        $data = $this->fetchHouseholdEstimatedData($startDate, $endDate);
        return $this->processHouseholdEstimatedData($data);
    }

    private function fetchHouseholdEstimatedData(\DateTime $startDate, \DateTime $endDate) 
    {
        /* Fetch data */ 
    }

    private function processHouseholdEstimatedData(array $data)
    {
        /* Process raw estimated data */
    }

    private function retrieveMacroActivities(\DateTime $startDate, \DateTime $endDate)
    {
        $data = $this->fetchMacroEstimatedData($startDate, $endDate);
        return $this->processMacroEstimatedData($data);
    }

    private function fetchMacroEstimatedData(\DateTime $startDate, \DateTime $endDate) 
    {
        /* Fetch data */
    }

    private function processMacroEstimatedData(array $data)
    {
        /* Process raw macro estimated data */
    }
}

The outcome is a service that is hard to follow and maintain. It is also more complex to test the different ways of retrieving the activities.

The Chain of Responsibility approach

The Chain of Responsibility pattern consists of having multiple services (called handlers) and running through them in a determined order to handle a request. Each service decides in runtime to either handle the request or pass it to the next handler. The CoR pattern can be used for instance in multi-stage validation or in managing multiple data providers.

Schema of the Chain of Responsibilities pattern

In our case, each data provider (meter reading, household consumption estimate, macro estimate) represents a “handler”. A request is handled if the service returns an array of activities.

Implementing activity providers using the Chain of Responsibilities pattern

To promote loose coupling, the 3 data providers will implement an interface that exposes a retrieveActivities function.

namespace Component\Activity\Provider;

interface ChainedActivityProviderInterface {
    public function retrieveActivities(\DateTime $startDate, \DateTime $endDate);
}

Each of the three services encapsulates the fetching and processing logic that match their respective data sources.

namespace Component\Activity\Provider;

class MeterReadingActivityProvider implements ChainedActivityProviderInterface {

    public function retrieveActivities(\DateTime $startDate, \DateTime $endDate): array
    {
        $data = $this->fetchData($startDate, $endDate);
        return $this->processData($data);
    }

    private function fetchData(\DateTime $startDate, \DateTime $endDate)
    {
        /* Process Data */
    }
    
    private function processData(array $data)
    {
        /* Process Data */
    }
}
namespace Component\Activity\Provider;

class HouseholdEstimateActivityProvider implements ChainedActivityProviderInterface {
    // Same structure as MeterReadingActivityProvider
}
namespace Component\Activity\Provider;

class MacroEstimatedElectricityActivityProvider implements ChainedActivityProviderInterface {
    // Same structure as MeterReadingActivityProvider
}

Lastly, to link the services, we can use a “main” activity provider in which they are injected and ran through in the injected order. As soon as one of the handlers returns a valid response (a non-empty array in this case), we return its value. Note that the main activity provider handles the case where none of the chained providers returns a valid response.

namespace Component\Activity\Provider;

class ActivityProvider {
    /**
     * @var ChainedActivityProviderInterface[]
     */
    private array $chainedActivityProviders;

    /**
     * @param ChainedActivityProviderInterface[] $chainedActivityProviders
     */
    public function __construct(array $chainedActivityProviders)
    {
        $this->chainedActivityProviders = $chainedActivityProviders;
    }

    /**
     * @return Activity[]
     */
    public function retrieveActivities(\DateTime $startDate, \DateTime $endDate): array
    {
        foreach ($this->chainedActivityProviders as $chainedActivityProvider) {
            $activities = $chainedActivityProvider->retrieveActivities($startDate, $endDate);
            if (!empty($activities)) {
                return $activities;
            }
        }

        return [];
    }
}

Finally, we need to inject the chained activity providers. In Symfony, one neat way of doing it is using the serices configuration file. This makes reordering as well as adding and removing the services an easy task.

# config/services.yaml
services:
    # ...

Component\Activity\Provider\ActivityProvider:
    arguments:
        $chainedActivityProviders: [
            '@Component\Activity\Provider\MeterReadingActivityProvider',
            '@Component\Activity\Provider\HouseholdEstimateActivityProvider',
            '@Component\Activity\Provider\MacroEstimatedElectricityActivityProvider'
        ]

With this setup in place, we call the retrieveActivities function of Component\Activity\Provider\ActivityProvider to retrieve a user’s activities.

The trade-offs of the CoR pattern

Implementing the Chain of Responsibility pattern offered many advantages:

However, the usage of the CoR pattern comes with a few drawbacks, namely:

Conclusion

In the long run, the Chain of Responsibility pattern helps organize and write modular code. However, it also comes with an initial cost higher than the trivial solution. They become interesting when the implemented system is prone to expansion. Indeed, setting up the chain is the most expensive part, while adding new chained elements keeps a constant cost over time.

Comparison of cost per change Trivial solution vs CoR

The implementation using injections is one of many ways to utilize this pattern. For implementations in other languages, refer to refactoring-guru.

Generally, design patterns should be perceived as blueprints that offer ready-to-use solutions to common problems. Since the human brain is built to work with patterns, understanding these solutions makes the code base more intelligible and easier to work with. However, one should be aware of the layer of complexity they can add due to their initial cost.

Liked this article?