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 eachelseif
condition – it’s alwaystrue
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 elseif
s
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