Reducing code complexity through good design

What is code complexity ?

Code complexity or cyclomatic complexity is the number of execution paths a program can take. Basically that is the number of if statements in a program or a method. There are many studies that show there is a direct correlation between the complexity of a program and the number of defects. The aim should be to reduce the complexity by reducing the number of if statements as much as possible.

Furthermore, having multiple if statements or ,even worse, nested conditionals make code hard to read and to maintain. Unit testing is also more complicated, because for each if statement you would have to follow multiple execution paths. The test number grows exponentially when having nested conditionals or switch statements.

How can we reduce the number of ifs ?

In oop programming, you can reduce the number of ifs by using polymorphism. This can move most of the conditional statements in factories that create the objects and out of the objects themselves. The tests should then check each object separately, and the factory just for creating the right object.

An example

I was recently working on a project that involved user subscriptions. A user could have a subscription or not, if the user had a subscription he could cancel or update it, and if he didn’t he could just create a subscription.

Please note that the examples are very simplified, because more complex code and business rules are involved in the actual application.

The user class looked like this:

class User
{
    protected $subscription;

    protected $email;

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

    public function getEmail()
    {
        return $this->email;
    }

    public function setSubscription(Subscription $subscription)
    {
        $this->subscription = $subscription;
    }

    public function registerSubscription($subscriptionDetails)
    {
        if($this->subscription) {
            throw new Exception('User already has a subscription');
        }
        //Code to register a subscription
    }

    public function cancelSubscription()
    {
        if(!$this->subscription) {
            throw new Exception('User doesn\'t have a subscription to cancel');
        }
        $this->subscription->cancel();
    }

    public function updateSubscription($newSubscriptionDetails)
    {
        if(!$this->subscription) {
            throw new Exception('User doesn\'t have a subscription to update');
        }
        $this->subscription->update($newSubscriptionDetails);
    }
}

You can see there are a lot of if statements that check if an user has a subscription or not. There are different paths to follow depending on this. To test this class we would need to write two tests for each method. One test with an user having subscription and one test with an user without one. We would have to assert that the registerSubscription method throws an exception when the user has already subscribed, and assert that it actually registers a subscription if the user doesn’t have a one.

We could replace all those if statements by realizing that there are two concepts bundled together in the same class. That of an user, and that of a subscriber. We can replace all those if statements with polymorphism by introducing the new concept of a Subscriber. The subscriber is an user, but has all the methods relating to an existing subscription inside of the class.

The new code would look like this:

class User
{
    protected $email;

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

    public function getEmail()
    {
        return $this->email;
    }

    public function registerSubscription($subscriptionDetails)
    {
        //Code to register a subscription
    }

}

class Subscriber extends User
{
    protected $subscription;

    public function __construct($email, Subscription $subscription)
    {
        $this->subscription = $subscription;
        parent::__construct($email);
    }

    public function registerSubscription($subscriptionDetails)
    {
        throw new Exception('User already has a subscription');
    }
    public function cancelSubscription()
    {
        $this->subscription->cancel();
    }

    public function updateSubscription($newSubscriptionDetails)
    {
        $this->subscription->update($newSubscriptionDetails);
    }
}

You can see that now all code relevant to a subscriber, like cancel or update subscription can be found in the Subsciber class, and the code that creates a subscription can be found in the User class. If you try to create a subscription for a subscriber it will throw an exception. This code is much easier to test and to maintain. All the logic of creating an user can now be moved to a factory class. If the user to be created has a subscription a Subscriber is created, and if not an User is created:

class UserFactory
{
    public function create($userDetails) {

        if(!$userDetails['subscription']) {
            return $this->createUser($userDetails);
        }
        else {
            return $this->createSubscriber($userDetails);
        }
    }

    private function createUser($userDetails)
    {
        $user = new User($userDetails['email']);
        return $user;
    }

    private function createSubscriber($userDetails)
    {
        $user = new Subscriber($userDetails['email'], $userDetails['subscription']);
        return $user;
    }
}

You can see that now, it’s the factory’s responsibility to decide what user to build, and it’s hard to create Subscribers or Users directly. It’s very easy to unit test this by sending dummy data to it, and asserting the type of user created.

An added benefit is that a new developer working on our code, won’t be able to create a Subscriber without sending a valid Subscription or an User without an email. We could extend this by adding a makeSubscriber(Subscription $subscription) method to the user class that will transform an user into a subscriber when he registers a subscription.

All the code is now much more clear, and it constraints developers from using it in the wrong way. A developer could still create a Subscriber without a subscription by using some complicated methods like Reflection, but that should be caught very easily in even the most summary code review.

Liskov substitution principle

When doing something like this it’s worth thinking about the Liskov substitution principle (The L in SOLID). The idea is that you must be able to substitute the User class with the Subscriber class in any context, and it shouldn’t produce unexpected behavior . For example, let’s say we have a mailer service that receives an user and sends an email notification.

class MailerService
{
    public function sendNotification(User $user)
    {
        $this->adaptor->sendEmail($user->getEmail(), "Hello !");
    }
}

This code should still work when sending a Subscriber to it. Because the Subscriber class extends from the User class, the Subscriber is still an User and the compiler won’t throw a typehinting error. If we look at the code, we can see that the subscriber inherits the $email property and getEmail() method from the User class so it preserves that behavior. Our mailer service will work regardless of the parameter type, it could be either an User or a Subscriber. The Liskov substitution principle is met in our example. More details on the Liskov Substitution principle will follow up in the next posts.

Conclusion

We can see how, with good design, we can drastically improve the testability and maintainability of the code. You should always look at multiple if statements or nested conditional as maybe a concept that’s hiding in the code. Bringing them to the surface can be very satisfying and helps creating SOLID code.

 

Ovidiu Maghetiu

Passionate Full-Stack Developer with more than 10 years experience in building complex web apps.

 
  • Many people wonder how they can improve the overall readability and cleanliness of their code. It seems impossible to understand exactly how to rewrite code in such a way that makes it clean, easy to understand and simple to work with. But clean code is about a few simple principles, one of which is reducing overall code complexity through a series of simple steps.