Refactoring of the “elseif” block code

Here’s the story. You’ve joined new project and you’ve 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 the code from 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;
    }
}

Discount value is provided based on order’s items count and order’s total value. Logic presented here doesn’t matter. The more important is:

  • idea to create method inside Order class for such 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 check 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 discount providing. No more responsibility for such logic in Order class that is a source of data. Besides that two interfaces has been created.

One for 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. Unnecessary $this->itemsCount >= 2 check 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 class has its own logic for discount. 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;
    }
}

Same applies for:

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 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 provider that iterates over rules and verifies if an order is qualified for 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 bounded 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 result of refactoring we’ve got a class that:

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

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