A null object removes the if you keep writing

An optional collaborator — a logger only configured in production, a cache that may be switched off, a discount that may not apply — produces the same three lines everywhere it is used: check for null, act, or do not. The check is not domain logic, and it is only correct for as long as every call site remembers to write it.

// the same guard, in eleven places
if ($this->logger !== null) {
    $this->logger->warning('Stock went negative for SKU ' . $sku);
}

interface Logger
{
    public function warning($message);
}

class NullLogger implements Logger
{
    public function warning($message) {}
}

// decided once, at construction
public function __construct(Logger $logger = null)
{
    $this->logger = $logger ?: new NullLogger();
}

// and from then on, unconditionally
$this->logger->warning('Stock went negative for SKU ' . $sku);

The stand-in has to satisfy the interface honestly, which means returning something the caller can use rather than null: a NullDiscount returns a zero amount, a NullCache returns a miss, and neither announces that it is a stand-in. That last part is what separates this from a flag — code doing instanceof NullLogger has reintroduced the conditional with extra steps. It is the wrong pattern wherever absence is a case the domain has an opinion about. An order with no customer is not an order with a null customer who silently agrees to everything, and hiding that behind a null object converts a loud bug into a quiet wrong answer. Use it where doing nothing is a legitimate behaviour, not where nothing was found.