Null-Checks durch ein neutrales Objekt ersetzen
Wiederkehrende Prüfungen wie if ($logger !== null) verteilen sich schnell über eine ganze Codebasis und verstecken die eigentliche Businesslogik hinter defensiver Programmierung. Das Null Object Pattern ersetzt fehlende Objekte durch ein neutrales Objekt mit demselben Interface, das sich einfach nichts tuend verhält, statt eine Fallunterscheidung zu erzwingen.
Inhaltsverzeichnis
- 1. Was das Null Object Pattern wirklich löst
- 2. Praxisbeispiel: ein neutraler NullLogger
- 3. Null Object bei Repository- und Finder-Mustern
- 4. Kombination mit dem Strategy Pattern
- 5. Ein Null Object als geteilte Singleton-Instanz
- 6. Grenzen: wann ein Null Object nicht passt
- 7. Abgrenzung zu Nullable Types und dem Nullsafe-Operator
- 8. Null Object in Tests: einfacher als Mocking für Randfälle
- 9. Null Object im Vergleich zu Alternativen
- 10. Zusammenfassung
- 11. FAQ
1. Was das Null Object Pattern wirklich löst
Das Null Object Pattern gehört zu den Verhaltensmustern und löst ein sehr alltägliches Problem: Statt für ein möglicherweise fehlendes Objekt an jeder Aufrufstelle eine explizite null-Prüfung zu erzwingen, wird eine konkrete Klasse bereitgestellt, die dasselbe Interface implementiert, aber ein neutrales, wirkungsloses Verhalten zeigt. Ein Null Object für einen Logger loggt einfach nichts, ein Null Object für einen Rabatt-Rechner gibt einfach null Rabatt zurück, ohne dass der aufrufende Code jemals wissen muss, ob ein "echtes" oder ein neutrales Objekt vorliegt.
Der Vorteil des Null Object Pattern liegt in der radikalen Vereinfachung des aufrufenden Codes. Statt if ($this->logger !== null) { $this->logger->info($message); } an zwanzig verschiedenen Stellen im Code zu wiederholen, ruft man einfach $this->logger->info($message) auf, in dem sicheren Wissen, dass $this->logger niemals null ist, sondern im Zweifel ein Null Object, das den Aufruf klaglos ignoriert. Diese Garantie, niemals mit tatsächlichem null umgehen zu müssen, ist der Kern dessen, was das Null Object Pattern an Wert bietet.
In diesem Artikel zeigen wir das Null Object Pattern an mehreren konkreten Beispielen, von einem einfachen Logger über Repository-Muster bis zur Kombination mit dem Strategy Pattern, und grenzen es klar von Nullable Types und dem Nullsafe-Operator ab, mit denen es häufig verwechselt wird, obwohl beide unterschiedliche Probleme lösen.
2. Praxisbeispiel: ein neutraler NullLogger
Der klassische Einstiegspunkt in das Null Object Pattern ist ein Logger, der optional an eine Klasse übergeben werden kann. Ohne Null Object Pattern müsste jede Methode, die potenziell loggt, zunächst prüfen, ob überhaupt ein Logger vorhanden ist. Mit dem Null Object Pattern wird stattdessen immer ein Logger übergeben, im Normalfall ein echter, im Fall fehlender Konfiguration ein NullLogger, der das LoggerInterface implementiert, aber jede Methode mit einem leeren Methodenkörper versieht.
Diese Technik ist in der PHP-Community so verbreitet, dass die PSR-3-Logging-Spezifikation selbst empfiehlt, Implementierungen mit einem NullLogger als Standardwert auszustatten. Bibliotheken, die einen Logger als optionale Abhängigkeit akzeptieren, nutzen dieses Muster durchgängig, damit der eigene Code niemals eine null-Prüfung für den Logger-Parameter benötigt. Das Null Object Pattern verschiebt die Verantwortung für die Behandlung des Fehlens vom aufrufenden Code in die Klassenkonstruktion, wo sie nur ein einziges Mal entschieden werden muss.
<?php
declare(strict_types=1);
interface LoggerInterface
{
public function info(string $message, array $context = []): void;
public function error(string $message, array $context = []): void;
}
// The Null Object — implements the interface, does effectively nothing
final class NullLogger implements LoggerInterface
{
public function info(string $message, array $context = []): void
{
// Intentionally empty — this is the whole point of the pattern
}
public function error(string $message, array $context = []): void
{
// Intentionally empty
}
}
final class OrderProcessor
{
// Defaults to NullLogger instead of allowing null — no null checks needed anywhere
public function __construct(
private readonly LoggerInterface $logger = new NullLogger(),
) {
}
public function process(Order $order): void
{
$this->logger->info("Processing order {$order->id}");
// ... business logic ...
$this->logger->info("Order {$order->id} processed successfully");
}
}
// Works identically whether a real logger or the default NullLogger is used
$processor = new OrderProcessor(); // silent
$processorWithLogging = new OrderProcessor(new FileLogger('/var/log/orders.log'));
3. Null Object bei Repository- und Finder-Mustern
Ein zweiter häufiger Einsatzbereich für das Null Object Pattern sind Repository- und Finder-Methoden, die möglicherweise kein Ergebnis liefern. Statt find() mit ?Customer als Rückgabetyp zu deklarieren und den Aufrufer zu zwingen, jedes Mal auf null zu prüfen, kann eine Methode ein NullCustomer-Objekt zurückgeben, das dasselbe Customer-Interface implementiert, aber neutrale Werte liefert, etwa einen leeren Namen und eine ungültige, aber typkorrekte E-Mail-Adresse.
Diese Anwendung des Null Object Pattern ist deutlich umstrittener als der Logger-Fall, weil das Fehlen eines Kunden häufig eine echte, fachlich relevante Information ist, die nicht einfach überdeckt werden sollte. Ein Checkout-Prozess, der versehentlich mit einem NullCustomer weiterarbeitet, statt den fehlenden Kunden als Fehler zu behandeln, kann zu inkonsistenten Bestellungen führen. Das Null Object Pattern eignet sich hier vor allem für Lesevorgänge mit geringer fachlicher Konsequenz, etwa das Anzeigen eines Platzhalter-Namens in einer Log-Zeile, nicht für kritische Geschäftsentscheidungen.
<?php
declare(strict_types=1);
interface CustomerInterface
{
public function getDisplayName(): string;
public function isRegistered(): bool;
}
final class RegisteredCustomer implements CustomerInterface
{
public function __construct(
private readonly string $firstName,
private readonly string $lastName,
) {
}
public function getDisplayName(): string
{
return "{$this->firstName} {$this->lastName}";
}
public function isRegistered(): bool
{
return true;
}
}
// Null Object for the "no customer found" case in low-stakes read paths
final class GuestCustomer implements CustomerInterface
{
public function getDisplayName(): string
{
return 'Guest';
}
public function isRegistered(): bool
{
return false;
}
}
final class CustomerRepository
{
public function findByEmail(string $email): CustomerInterface
{
$row = $this->connection->fetchOne('SELECT * FROM customers WHERE email = :email', ['email' => $email]);
// Returns a Null Object instead of null — no null-check burden on the caller
return $row !== false
? new RegisteredCustomer($row['first_name'], $row['last_name'])
: new GuestCustomer();
}
}
4. Kombination mit dem Strategy Pattern
Das Null Object Pattern lässt sich hervorragend mit dem Strategy Pattern kombinieren, insbesondere dort, wo eine optionale Verhaltensvariante als Strategie modelliert wird. Ein Beispiel: Ein Rabattsystem definiert eine DiscountStrategyInterface mit einer Methode calculate(Order $order): Money. Statt zu prüfen, ob überhaupt eine Rabattstrategie zugewiesen wurde, wird bei fehlendem Rabatt ein NoDiscountStrategy-Objekt verwendet, das schlicht Money::zero() zurückgibt.
Diese Kombination aus Strategy und Null Object Pattern macht den aufrufenden Code radikal einfacher: $order->applyDiscount($strategy->calculate($order)) funktioniert identisch, egal ob ein echter Rabatt oder gar kein Rabatt vorliegt. Der bedingte Zweig, der sonst prüfen müsste, ob überhaupt ein Rabatt existiert, verschwindet vollständig aus der Businesslogik und wandert stattdessen in die Entscheidung, welche Strategie-Instanz bei der Objekterstellung ausgewählt wird.
<?php
declare(strict_types=1);
interface DiscountStrategyInterface
{
public function calculate(Order $order): Money;
}
final class PercentageDiscountStrategy implements DiscountStrategyInterface
{
public function __construct(private readonly float $percentage)
{
}
public function calculate(Order $order): Money
{
return $order->getSubtotal()->multiply($this->percentage / 100);
}
}
// Null Object: same interface, zero effect — no conditional branching needed
final class NoDiscountStrategy implements DiscountStrategyInterface
{
public function calculate(Order $order): Money
{
return Money::zero($order->getSubtotal()->getCurrency());
}
}
final class Order
{
private DiscountStrategyInterface $discountStrategy;
public function __construct()
{
$this->discountStrategy = new NoDiscountStrategy();
}
public function applyDiscountStrategy(DiscountStrategyInterface $strategy): void
{
$this->discountStrategy = $strategy;
}
public function getTotal(): Money
{
// Works identically for real discounts and the "no discount" Null Object
return $this->getSubtotal()->subtract($this->discountStrategy->calculate($this));
}
}
5. Ein Null Object als geteilte Singleton-Instanz
Da ein Null Object per Definition zustandslos ist und keine internen Daten hält, die zwischen verschiedenen Verwendungen variieren müssten, bietet es sich häufig an, nur eine einzige geteilte Instanz statt vieler neuer Objekte zu verwenden. Ein statischer Fabrik-Methodenaufruf wie NullLogger::instance() mit interner Singleton-Verwaltung spart unnötige Objekterzeugung, ohne dass die Semantik des Null Object Pattern sich ändert.
Diese Optimierung ist besonders in Codepfaden relevant, die häufig durchlaufen werden, etwa bei jedem HTTP-Request. Wichtig dabei: Die geteilte Instanz darf wirklich keinen veränderlichen Zustand besitzen, sonst käme es zu unerwarteten Seiteneffekten zwischen verschiedenen Verwendern desselben Objekts. Für ein echtes Null Object, das definitionsgemäß nichts tut, ist diese Bedingung praktisch immer erfüllt, was die Singleton-Optimierung risikofrei macht.
6. Grenzen: wann ein Null Object nicht passt
Das Null Object Pattern ist kein universelles Werkzeug gegen jede Form von null. Wo das Fehlen eines Werts eine fachlich wichtige Information trägt, die eine bewusste Entscheidung des aufrufenden Codes erfordert, verschleiert ein Null Object genau diese notwendige Entscheidung. Ein Zahlungsservice, der bei fehlender Zahlungsmethode ein NullPaymentMethod zurückgibt, das stillschweigend keine Zahlung verarbeitet, versteckt einen kritischen Fehlerzustand, der eigentlich eine Exception oder zumindest eine explizite Fehlerbehandlung verdient hätte.
Ein zweites Problem entsteht, wenn zu viele verschiedene Null Object-Varianten für dieselbe Schnittstelle existieren, jeweils mit leicht unterschiedlichem neutralem Verhalten. Das führt zu Verwirrung darüber, welches "Nichts" in welchem Kontext gemeint ist. Die Faustregel: Das Null Object Pattern eignet sich für Fälle, in denen "nichts tun" oder "neutraler Wert" eine sinnvolle, unmissverständliche Semantik hat, nicht für Fälle, in denen Abwesenheit eigentlich ein Fehlerzustand ist, der Sichtbarkeit statt Verschleierung braucht.
7. Abgrenzung zu Nullable Types und dem Nullsafe-Operator
Das Null Object Pattern wird häufig mit Nullable Types (?Type) und dem Nullsafe-Operator (?->) verwechselt, obwohl beide unterschiedliche Probleme lösen. Nullable Types und der Nullsafe-Operator machen den Umgang mit tatsächlichem null bequemer, indem sie Ketten wie $user?->getAddress()?->getCity() ohne verschachtelte if-Prüfungen erlauben. Das Null Object Pattern hingegen vermeidet null komplett, indem es durch ein echtes Objekt mit neutralem Verhalten ersetzt wird.
Der entscheidende Unterschied zeigt sich beim Methodenaufruf: Mit dem Nullsafe-Operator liefert eine Kette wie $user?->getLogger()?->info($message) bei fehlendem Logger einfach null zurück und tut nichts, was auf den ersten Blick ähnlich wirkt wie das Null Object Pattern. Der Unterschied liegt in der Konsistenz: Der Nullsafe-Operator muss an jeder einzelnen Aufrufstelle wiederholt werden, während das Null Object Pattern die Entscheidung genau einmal bei der Objekterstellung trifft und danach überall im Code einfach normale Methodenaufrufe erlaubt, ohne ?-> an jeder Stelle wiederholen zu müssen.
8. Null Object in Tests: einfacher als Mocking für Randfälle
Ein oft übersehener Vorteil des Null Object Pattern zeigt sich beim Testen von Randfällen. Statt für jeden Test, der das Fehlen eines optionalen Objekts simulieren soll, ein Mock-Objekt mit einer null-Rückgabe zu konfigurieren, kann man einfach die reguläre Null Object-Implementierung injizieren. Das reduziert Testcode-Boilerplate und macht Tests robuster gegenüber Refactorings, weil das Null Object Teil der Produktionscodebasis ist und sich mit ihr weiterentwickelt, statt in jedem Test isoliert neu konfiguriert werden zu müssen.
Diese Eigenschaft macht das Null Object Pattern besonders attraktiv in Testsuiten, die viele Varianten "mit und ohne optionale Abhängigkeit" abdecken müssen. Ein Test für OrderProcessor ohne Logger muss keinen Mock aufsetzen, sondern nutzt einfach den Standardwert new NullLogger(), der ohnehin schon Teil der Klassendefinition ist. Das spart nicht nur Zeilen, sondern verringert auch die Wahrscheinlichkeit, dass ein Mock versehentlich anderes Verhalten zeigt als die tatsächliche Produktionsimplementierung des Null Object.
9. Null Object im Vergleich zu Alternativen
Die folgende Tabelle vergleicht das Null Object Pattern mit den gängigsten Alternativen für den Umgang mit fehlenden Werten und Objekten in PHP.
| Ansatz | Aufrufer-Aufwand | Fehler sichtbar? | Passend für |
|---|---|---|---|
| Explizite null-Prüfung | Hoch, an jeder Stelle wiederholt | Ja, wenn behandelt | Kritische Fehlerzustände |
| Nullsafe-Operator | Mittel, ?-> an jeder Stelle | Teilweise, still bei Kettenabbruch | Kurze Zugriffsketten |
| Exception werfen | Gering, try/catch zentral | Ja, explizit und laut | Fachlich kritisches Fehlen |
| Null Object Pattern | Sehr gering, normaler Aufruf | Nein, bewusst unsichtbar | Optionale, unkritische Abhängigkeiten |
Die Tabelle macht deutlich: Das Null Object Pattern minimiert den Aufwand für den aufrufenden Code am stärksten, verschleiert dafür aber bewusst das Fehlen. Genau deshalb ist die Wahl zwischen den vier Ansätzen keine reine Stilfrage, sondern hängt davon ab, ob das Fehlen eines Objekts fachlich relevant ist oder tatsächlich neutral behandelt werden darf.
Mironsoft
PHP-Architektur, defensive Programmierung und Code-Reviews
Codebasis voller null-Checks statt klarer Objektlogik?
Wir identifizieren wiederkehrende null-Prüfungen in eurer PHP-Codebasis, unterscheiden zwischen echten Fehlerzuständen und unkritischen optionalen Abhängigkeiten, und ersetzen letztere gezielt durch saubere Null-Object-Implementierungen.
Code-Audit
Null-Check-Muster identifizieren und nach fachlicher Relevanz klassifizieren
Refactoring
Unkritische Fälle durch Null-Object-Implementierungen ersetzen
Test-Strategie
Null-Object-Instanzen für schlanke, robuste Randfall-Tests etablieren
10. Zusammenfassung
Das Null Object Pattern ersetzt fehlende Objekte durch eine konkrete Implementierung mit demselben Interface, die sich neutral verhält, statt eine explizite null-Prüfung an jeder Aufrufstelle zu erzwingen. Für optionale, unkritische Abhängigkeiten wie Logger oder Rabattstrategien reduziert das Null Object Pattern den aufrufenden Code auf normale Methodenaufrufe, ohne jemals mit echtem null umgehen zu müssen.
Die Grenze des Musters liegt dort, wo das Fehlen eines Objekts eine fachlich wichtige Information trägt, die eine bewusste Entscheidung erfordert. Hier verschleiert das Null Object Pattern genau die Information, die sichtbar gemacht werden sollte, und eine Exception oder explizite Fehlerbehandlung ist die bessere Wahl. Wer diese Grenze klar zieht, nutzt das Null Object Pattern gezielt dort, wo es Code wirklich vereinfacht, ohne wichtige Fehlerzustände zu verstecken.
Null Object Pattern in PHP — Das Wichtigste auf einen Blick
Kernidee
Ein neutrales Objekt mit demselben Interface ersetzt tatsächliches null und macht null-Prüfungen überflüssig.
Typische Beispiele
NullLogger, GuestCustomer, NoDiscountStrategy, kombinierbar mit dem Strategy Pattern.
Nicht verwechseln mit
Nullable Types und Nullsafe-Operator lösen den Umgang mit echtem null, nicht dessen Vermeidung.
Grenze
Nicht einsetzen, wenn Fehlen ein fachlich kritischer Zustand ist, der explizite Fehlerbehandlung braucht.