Last Updated on 14/11/2020
Null Object is a behavioural design pattern based on inheritance which creates a valid representation of null
objects in a system, in order to avoid returning null
objects, on which respective null checks would be needed to prevent NullPointerException
and unexpected behaviour of the application objects.
Introduction
It’s common to write methods that return null
in situations where the requested information is not present or some conditions are not met in order to execute some chunk of code. However, sometimes this behaviour is poorly documented so it takes the developers using a given API by surprise, moreover, it can force that said developers to write lots of null checks to avoid runtime exceptions.
On either way, the application code might end up with a lack of cohesion and not clean at all, because now this chunk of code has to deal with a “null possible situation” and take decisions that would not be supposed to be taken by itself.
The Null Object design pattern comes to work on this problem, basically, instead of returning null
where an object of class Foo
was expected, one could return an object of a subclass of Foo
in a basic valid state but at the same time, adhering to Foo
‘s contract.
Null Object Structure
Imagine we have an online shop, every day we search the database for discounts that could be applied to the products being sold. This way, we are going to have a Discount class which we are going to inherit from in order to apply the Null Object pattern.
Below is the UML representation:
Important considerations about the diagram:
- CheckOut in just an example of a client class which is going to rely on the advantages of this design pattern, therefore, it could be any class;
- The methods from NullDiscount override the ones from Discount class adhering to its contract;
- I can’t see any problem with implementing this design pattern on an interface instead of subclassing, but it’s more commonly applied over domain objects which often happen to be classes.
Implementation
Below are the Java classes which implement the pattern. As usual, some javadoc, comments, getters, setters, constructors and toString
overwritten were omitted for readability purposes. Please refer to the GitHub repository for the full version of this code.
Discount: the domain class that is going to be populated with the discount information we find (if any):
package com.bgasparotto.designpatterns.nullobject; import java.math.BigDecimal; import java.time.LocalDateTime; public class Discount { private BigDecimal value; private LocalDateTime date; public BigDecimal getValue() { return value; } public LocalDateTime getDate() { return date; } // Setters. }
NullDiscount: the class which implements the pattern based on Discount
class:
package com.bgasparotto.designpatterns.nullobject; import java.math.BigDecimal; import java.time.LocalDateTime; public class NullDiscount extends Discount { @Override public BigDecimal getValue() { return BigDecimal.ZERO; } @Override public LocalDateTime getDate() { return LocalDateTime.now(); } }
CheckOut: the client class which is going to benefit from the pattern class:
package com.bgasparotto.designpatterns.nullobject; import java.math.BigDecimal; import java.time.LocalDateTime; public class CheckOut { private BigDecimal cartValue; private Discount discount; public CheckOut(BigDecimal cartValue) { this.cartValue = cartValue; } public void doCheckOut() { DiscountService discountService = new DiscountService(); discount = discountService.findDiscount(LocalDateTime.now()); // Null check without the pattern. BigDecimal discountValue = discount.getValue(); cartValue = cartValue.subtract(discountValue); System.out.println("Final cart value is " + cartValue); // Null check without the pattern. System.out.println("You got a discount of " + discountValue); } public static void main(String[] args) { CheckOut checkOut = new CheckOut(new BigDecimal(1000)); checkOut.doCheckOut(); } }
Last, a service class that wasn’t on the diagram but it’s responsible for finding and returning the available discounts. Since we are just discussing patterns, I’ve mocked the “database” search:
package com.bgasparotto.designpatterns.nullobject; import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.concurrent.ThreadLocalRandom; public class DiscountService { public Discount findDiscount(LocalDateTime date) { // Goes to the mock database and checks for today's discounts. Discount discount = findMockDiscount(date); return discount; } private Discount findMockDiscount(LocalDateTime date) { // Gets a random number and return null if it's even int random = ThreadLocalRandom.current().nextInt(0, 9 + 1); if (random % 2 == 0) { return null; } // Returns a valid discount object otherwise Discount discount = new Discount(); discount.setValue(BigDecimal.TEN); discount.setDate(date); return discount; } }
Discussion
First, please note the following default value declaration and the null check:
// Default value in case the discount is null BigDecimal discountValue = BigDecimal.ZERO; // Null check without the pattern. if (discount != null) { discountValue = discount.getValue(); } cartValue = cartValue.subtract(discountValue);
We can say they exist because we have a lack of cohesion here, the DiscountService class were supposed to give us a discount object, but instead, it may return null because the service wasn’t able to find the discount and leave the problem to us to deal with it.
Now imagine a big software with these kinds of checks at every single client class that needs to search for discounts? We might end up with lots of duplicated code, because since one null check is working, the tendency is to copy and paste the same check in the new piece of code we are developing, but since everyone should know, when you copy and paste something in an object oriented code, something has seriously gone wrong!
What if we remove these checks? For starters, the code is going to look cleaner, so let’s stick with this idea and take it as our first step to apply the pattern:
// Null checks no more BigDecimal discountValue = discount.getValue(); cartValue = cartValue.subtract(discountValue);
Next, we are going to centralise the removed checks in the DiscountService class, so any client class can benefit from it. So instead of:
Discount discount = findMockDiscount(date); return discount;
We are gonna return our pattern implementation class if our mock method returns null:
if (discount == null) { return new NullDiscount(); } return discount;
The rule here is to return the null representation of the object at the first business piece of code you would return null
instead, to avoid propagating any nulls down to your client classes.
Finally, on Java 8 you can use Optional to get rid of the last null check:
Optional<Discount> optional = Optional.ofNullable(discount); return optional.orElse(new NullDiscount());
Conclusion
Don’t fool yourself if the difference between the no-pattern implementation and the one with null object seems to be small, remember this is just an example in a very small software with educational purposes.
In real software, the classes are likely to be way bigger with lots of attributes to check against null values, so you might have dozens of lines of code checking these attributes.
Therefore, we’ve increased the cohesion of our code by giving to the DiscountService class the responsibility to deal with null values, having the approach centred in just one class. That said, any future maintenance is this rule is gonna get simpler because we have just one place to change without the need to deal with the client code directly.