Refactoring of the “elseif” block code

Here’s the story. You’ve joined a new project and been provided a source code. You’re so excited and full of energy.

Welcome to the real world

You’re so excited until… you pull the code from a repository and open a class like this one:

class Order
{
    // ...

    public function getDiscount(): int
    {
        if ($this->itemsCount < 2) {
            return 0;
        } elseif ($this->itemsCount >= 2 && $this->total >= 50 && $this->total < 100) {
            return 5;
        } elseif ($this->itemsCount >= 2 && $this->total >= 100 && $this->total < 1000) {
            return 10;
        } elseif ($this->itemsCount >= 2 && $this->total >= 1000 && $this->total < 5000) {
            return 15;
        } elseif ($this->itemsCount >= 2 && $this->total >= 5000) {
            return 20;
        }

        return 0;
    }
}

The discount value is based on the order’s item count and total value. The logic presented here doesn’t matter. The more important is:

  • the idea is to create a method inside the Order class for such a discount
  • verification implemented via elseif conditions
  • unnecessary $this->itemsCount >= 2 in each elseif condition – it’s always true because we’ve already verified at the beginning if $this->itemsCount < 2

First, small step

Let’s remove unnecessary $this->itemsCount >= 2 condition and create a separate class.

class DiscountProvider implements DiscountProviderInterface
{
    public function provide(OrderInterface $order): int
    {
        if ($order->getItemsCount() < 2) {
            return 0;
        } elseif ($order->getTotal() >= 50 && $order->getTotal() < 100) {
            return 5;
        } elseif ($order->getTotal() >= 100 && $order->getTotal() < 1000) {
            return 10;
        } elseif ($order->getTotal() >= 1000 && $order->getTotal() < 5000) {
            return 15;
        } elseif ($order->getTotal() >= 5000) {
            return 20;
        }

        return 0;
    }
}

The new class is now responsible for providing discounts. No more responsibility for such logic in Order class that is a data source. Besides that, two interfaces have been created.

One for the provider:

interface DiscountProviderInterface
{
    public function provide(OrderInterface $order): int;
}

And one for order:

interface OrderInterface
{
    public function getItemsCount(): int;

    public function getTotal(): float;
}

Split the elseifs

Many elseif conditions make the code unreadable. It’s harder to understand what’s going on there. The unnecessary $this->itemsCount >= 2 condition was a negative result of such “condensed” code, presumably.

class DiscountProvider implements DiscountProviderInterface
{
    public function provide(OrderInterface $order): int
    {
        if ($order->getItemsCount() < 2) {
            return 0;
        }

        if ($order->getTotal() >= 50 && $order->getTotal() < 100) {
            return 5;
        }

        if ($order->getTotal() >= 100 && $order->getTotal() < 1000) {
            return 10;
        }

        if ($order->getTotal() >= 1000 && $order->getTotal() < 5000) {
            return 15;
        }

        if ($order->getTotal() >= 5000) {
            return 20;
        }

        return 0;
    }
}

We can see 5 blocks of if () code now. Each of them may be a separate mechanism for verification of discount value:

if ($order->getItemsCount() < 2) {
    return 0;
}
if ($order->getTotal() >= 50 && $order->getTotal() < 100) {
    return 5;
}

and so on.

Introduce discount rule

Instead of if () code blocks, we can implement separate classes. Each of the classes has its logic for discount calculation. Let’s call them DiscountRule.

Instead of:

if ($order->getItemsCount() < 2) {
    return 0;
}

We can implement this class:

class NoDiscountRule implements DiscountRuleInterface
{
    public function getDiscount(): int
    {
        return 0;
    }

    public function isQualified(OrderInterface $order): bool
    {
        return $order->getItemsCount() < 2;
    }
}

The same applies to:

if ($order->getTotal() >= 50 && $order->getTotal() < 100) {
    return 5;
}

We replace with:

class MiniDiscountRule implements DiscountRuleInterface
{
    public function getDiscount(): int
    {
        return 5;
    }

    public function isQualified(OrderInterface $order): bool
    {
        return $order->getTotal() >= 50 && $order->getTotal() < 100;
    }
}

and so on.

Yes, we’ve got a new interface for the discount rule:

interface DiscountRuleInterface
{
    public function getDiscount(): int;

    public function isQualified(OrderInterface $order): bool;
}

Implement rules in discount provider

We can implement our rules in the provider that iterates over rules and verifies if an order is qualified for a particular order.

class DiscountProvider implements DiscountProviderInterface
{
    /** @var DiscountRuleInterface[] */
    private array $rules;

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

    public function provide(OrderInterface $order): int
    {
        if (empty($this->rules)) {
            return 0;
        }

        foreach ($this->rules as $rule) {
            if (!$rule->isQualified($order)) {
                continue;
            }

            return $rule->getDiscount();
        }

        return 0;
    }
}

Provider does not contain any specific logic bound to an order or discount. It’s a “controller” that connects discount rule and order together. It’s simple: no rules, no discount. Rule found? Let’s provide the discount.

Summary

We’ve started with tough to debug and maintain blocks of elseif code. As a result of refactoring, we’ve got a class that:

  • has single responsibility
  • is open for extension, closed for modification
  • implements an interface, so we’re ready to create more than one implementation of the interface

Source code: https://github.com/kniziol/blog-refactoring-of-elseif