Skip to content
Permalink
Branch: master
Find file Copy path
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
177 lines (134 sloc) 5.5 KB
id title description sources
5
Null Object design pattern
An object with no value or defined behavior can be helpful when no other "real" object is present so it can mimic its usage. It may help you to avoid unnecessary conditionals and make your code more readable.
Chasing "Perfect" presentation
Pushing Polymorphism to the Database screencast
Null Object Design Pattern on sourcemaking.com
Null Object Design Pattern on wikipedia.com

Recently I saw a presentation called Chasing "Perfect" given by Adam Wathan at Laracon EU in 2015. In case you don't know him I strongly recommend you to follow him on Twitter, at least. I definitely did so.

Consider this article as my notes or thoughts on presented topic so I won't forget this whole thing in the future.

The idea

In the presentation, Adam illustrated how to structure application code when new requirements keep come around. In his example, a book ordering system can use coupons to give a discount for a customer.

I will use exact same code to present the initial conditions (quite simplified, of course):

class Order
{
    private $books;

    private $coupon;

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

    public function applyCoupon($coupon)
    {
        $this->coupon = $coupon;
    }

    public function total()
    {
        $discount = 0;

        if (isset($this->coupon)) {
            $discount = $this->coupon->value;
        }

        return $this->books->sum('price') - $discount;
    }
}

So the Order::total() method calculates the total value of ordered books and in a case a coupon is applied, it subtracts total by the value of a coupon. Pretty straightforward.

Now, let's imagine a new requirement from your boss: "we need to implement a new coupon that is percentage based". You say "no problem" and enhance the code in total() method (or maybe not, but it might be tempting to make such an easy change):

public function total()
{
    $discount = 0;

    if (isset($this->coupon)) {
        if ($this->coupon->isPercentage()) {
            $discount = $this->books->sum('price') * ($this->coupon->value / 100);
        } else {
            $discount = $this->coupon->value;
        }
    }

    return $this->books->sum('price') - $discount;
}

There is no null object yet, but we are getting to the point. We can see a lot of conditions which are not very popular (yes, because nested conditions are ugly). The problem in this example can only grow when new requirements come, not even switch statement can help us solve this problem nicely.

The best we can do is to extract discount logic somewhere else and in that way introduce Coupon classes - one class for every type of a coupon. And after several rounds of refactoring here is the result:

class ValueCoupon
{
    public function discount($order)
    {
        return $this->value;
    }
}

class PercentageCoupon
{
    public function discount($order)
    {
        return $order->grossTotal() * ($this->value / 100);
    }
}

Order::class has also changed a bit:

class Order
{
    // ...

    public function grossTotal($order)
    {
        return $this->books->sum('price');
    }

    public function total()
    {
        return $this->grossTotal() - $this->discount();
    }

    private function discount()
    {
        if (isset($this->coupon)) {
            return $this->coupon->discount($this);
        }

        return 0;
    }
}

So, what has changed exactly?

  • new Coupon classes have only one discount() method that consists of only one line of code
  • added Order::grossTotal() to get value of the books
  • almost all methods in Order::class are really simple, except Order::discount()

Maybe it would be better to implement also an interface ICoupon, but this is another story.

The solution

I would be personally quite happy with this code and leave it as it is. But here comes the whole point - we can get rid of that one additional condition with the usage of the Null Object pattern.

class NullCoupon
{
    public function discount($order)
    {
        return 0;
    }
}

class Order
{
    public function __construct()
    {
        $this->books = $books;
        $this->coupon = new NullCoupon();
    }

    // ...

    private function discount()
    {
        return $this->coupon->discount($this);
    }
}

Instead of always checking if something is null or not present and react differently in such a situation you can deliberately implement a behavior that represents, well, nothing. And the Null Object exists to solve such specific situations. When Order::applyCoupon() is used, it rewrites NullCoupon with a new one and the usage remains same.

Another useful application of a Null Object could be to create it as a stub in your tests.

It does not end here

Below the video, one of the commenters pointed out a question: "How can we instantiate a coupon based on the user input with the same approach?"

Adam responded with another screencast and came with Database Polymorphism implemented with Laravel Eloquent. A remarkably elegant solution, I would say. For those of you, who work with Laravel, it might be truly educational.

You can’t perform that action at this time.