Purpose
The purpose of the design pattern
NullObject is not a GoF (gang of four) design pattern but a schema which appears frequently enough to be considered a pattern. It has the following benefits:
- Client code is simplified
- Reduces the chance of null pointer exceptions
- Fewer conditionals require less test cases
Methods that return an object or null should instead return an object or NullObject.
NullObjects simplify boilerplate code such as if (!is_null($obj)) { $obj->callSomething(); to just: $obj->callSomething(); by eliminating the conditional check in client code.
Examples
Examples of how the design pattern can be used
- Null logger or null output to preserve a standard way of interaction between objects, even if the shouldn't do anything
- Null handler in a Chain of Responsibilities pattern
- Null command in a Command pattern
UML
UML design pattern diagram
Code
Code snippets
Service
Service class accepts a Logger via its construct, the doSomething method calls the log method contained within the Logger instance.
namespace DesignPatterns\Behavioral\NullObject;
class Service
{
public function __construct(private Logger $logger)
{
}
/**
* do something ...
*/
public function doSomething()
{
// notice here that you don't have to check if the logger is set with eg. is_null(), instead just use it
$this->logger->log('We are in ' . __METHOD__);
}
}
Logger
Logger interface that NullLogger and PrintLogger implement, ensuring these concrete classes have a log method.
namespace DesignPatterns\Behavioral\NullObject;
/**
* Key feature: NullLogger must inherit from this interface like any other loggers
*/
interface Logger
{
public function log(string $str);
}
Null Logger
NullLogger implements Logger (but does nothing)
namespace DesignPatterns\Behavioral\NullObject;
class NullLogger implements Logger
{
public function log(string $str)
{
// do nothing
}
}
Print Logger
PrintLogger implements Logger
namespace DesignPatterns\Behavioral\NullObject;
class PrintLogger implements Logger
{
public function log(string $str)
{
echo $str;
}
}
Tests
Unit tests showing the code above in action. Instead of a method which returns an object or null, you’d always return an object (and if required a null object). This approach also doesn’t break the Liskov Substitution Principle (LSP).
public function testNullObject()
{
$service = new Service(new NullLogger());
$this->expectOutputString('');
$service->doSomething();
}
public function testStandardLogger()
{
$service = new Service(new PrintLogger());
$this->expectOutputString('We are in DesignPatterns\Behavioral\NullObject\Service::doSomething');
$service->doSomething();
}