Nullobjektmuster - Null object pattern

In der objektorientierten Computerprogrammierung ist ein Nullobjekt ein Objekt ohne Referenzwert oder mit definiertem neutralem ("null") Verhalten. Das Null-Objekt- Entwurfsmuster beschreibt die Verwendung solcher Objekte und ihr Verhalten (oder deren Fehlen). Es wurde erstmals in der Buchreihe Pattern Languages ​​of Program Design veröffentlicht .

Motivation

In den meisten objektorientierten Sprachen wie Java oder C # , Referenzen können null . Diese Referenzen müssen vor dem Aufrufen von Methoden überprüft werden, um sicherzustellen, dass sie nicht null sind , da Methoden normalerweise nicht auf null-Referenzen aufgerufen werden können.

Die Objective-C-Sprache geht einen anderen Ansatz für dieses Problem und tut nichts, wenn eine Nachricht an gesendet wird nil; wenn ein Rückgabewert erwartet wird nil(für Objekte), 0 (für numerische Werte), NO(für BOOLWerte) oder eine Struktur (für Strukturtypen) mit all ihren Membern initialisiert auf null/0/ NO/null-initialisierte Struktur wird zurückgegeben.

Beschreibung

Anstatt eine Nullreferenz zu verwenden, um das Fehlen eines Objekts (z. B. einen nicht existierenden Kunden) zu signalisieren, verwendet man ein Objekt, das die erwartete Schnittstelle implementiert , dessen Methodenrumpf jedoch leer ist. Der Vorteil dieses Ansatzes gegenüber einer funktionierenden Standardimplementierung besteht darin, dass ein Nullobjekt sehr vorhersehbar ist und keine Nebenwirkungen hat: Es tut nichts .

Beispielsweise kann eine Funktion eine Liste von Dateien in einem Ordner abrufen und für jede eine Aktion ausführen. Im Fall eines leeren Ordners kann eine Reaktion darin bestehen, eine Ausnahme auszulösen oder statt einer Liste einen Nullverweis zurückzugeben. Daher muss der Code, der eine Liste erwartet, überprüfen, ob er tatsächlich eine hat, bevor er fortfährt, was den Entwurf verkomplizieren kann.

Indem stattdessen ein Nullobjekt (dh eine leere Liste) zurückgegeben wird, muss nicht überprüft werden, ob der Rückgabewert tatsächlich eine Liste ist. Die aufrufende Funktion kann die Liste einfach wie gewohnt durchlaufen und effektiv nichts tun. Es ist jedoch weiterhin möglich zu prüfen, ob der Rückgabewert ein Null-Objekt (eine leere Liste) ist und gegebenenfalls anders zu reagieren.

Das Nullobjektmuster kann auch als Stub zum Testen verwendet werden, wenn ein bestimmtes Feature, wie z. B. eine Datenbank, nicht zum Testen verfügbar ist.

Beispiel

Gegeben einen binären Baum mit dieser Knotenstruktur:

class node {
    node left
    node right
}

Man kann eine Baumgrößenprozedur rekursiv implementieren:

function tree_size(node) {
    return 1 + tree_size(node.left) + tree_size(node.right)
}

Da die untergeordneten Knoten möglicherweise nicht existieren, muss man die Prozedur ändern, indem man Nicht-Existenz- oder Null-Prüfungen hinzufügt:

function tree_size(node) {
    set sum = 1
    if node.left exists {
        sum = sum + tree_size(node.left)
    }
    if node.right exists {
        sum = sum + tree_size(node.right)
    }
    return sum
}

Dies macht jedoch das Verfahren komplizierter, indem Grenzüberprüfungen mit normaler Logik gemischt werden, und es wird schwieriger zu lesen. Mit dem Nullobjektmuster kann man eine spezielle Version der Prozedur erstellen, aber nur für Nullknoten:

function tree_size(node) {
    return 1 + tree_size(node.left) + tree_size(node.right)
}
function tree_size(null_node) {
    return 0
}

Dies trennt die normale Logik von der Behandlung von Sonderfällen und macht den Code leichter verständlich.

Beziehung zu anderen Mustern

Es kann als Sonderfall des State Pattern und des Strategy Pattern angesehen werden .

Es ist kein Muster aus Design Patterns , sondern wird in Martin Fowlers Refactoring und Joshua Kerievskys Refactoring To Patterns als Insert Null Object Refactoring erwähnt .

Kapitel 17 von Robert Cecil Martins Agile Software Development: Principles, Patterns and Practices ist diesem Muster gewidmet.

Alternativen

Ab C# 6.0 ist es möglich, das "?." Operator (auch als null-bedingter Operator bekannt ), der einfach zu null ausgewertet wird, wenn sein linker Operand null ist.

// compile as Console Application, requires C# 6.0 or higher
using System;

namespace ConsoleApplication2
{
    class Program
    {
        static void Main(string[] args)
        {
            string str = "test"; 
            Console.WriteLine(str?.Length);
            Console.ReadKey();
        }
    }
}
// The output will be:
// 4

Erweiterungsmethoden und Null-Coalescing

In einigen Microsoft .NET- Sprachen können Erweiterungsmethoden verwendet werden, um das sogenannte „Null-Coalescing“ durchzuführen. Dies liegt daran, dass Erweiterungsmethoden für Nullwerte aufgerufen werden können, als ob es sich um einen "Instanzmethodenaufruf" handelt, während Erweiterungsmethoden tatsächlich statisch sind. Erweiterungsmethoden können so eingerichtet werden, dass sie nach Nullwerten suchen, wodurch Code, der sie verwendet, dies jemals tun muss. Beachten Sie, dass das folgende Beispiel den C# -Null-Koaleszenz-Operator verwendet , um einen fehlerfreien Aufruf zu gewährleisten, wobei auch ein banaleres if...then...else hätte verwendet werden können. Das folgende Beispiel funktioniert nur, wenn Ihnen die Existenz von null egal ist oder Sie null und eine leere Zeichenfolge gleich behandeln. Die Annahme kann in anderen Anwendungen nicht gelten.

// compile as Console Application, requires C# 3.0 or higher
using System;
using System.Linq;
namespace MyExtensionWithExample {
    public static class StringExtensions { 
        public static int SafeGetLength(this string valueOrNull) { 
            return (valueOrNull ?? string.Empty).Length; 
        }
    }
    public static class Program {
        // define some strings
        static readonly string[] strings = new [] { "Mr X.", "Katrien Duck", null, "Q" };
        // write the total length of all the strings in the array
        public static void Main(string[] args) {
            var query = from text in strings select text.SafeGetLength(); // no need to do any checks here
            Console.WriteLine(query.Sum());
        }
    }
}
// The output will be:
// 18

In verschiedenen Sprachen

C++

Eine Sprache mit statisch typisierten Verweisen auf Objekte veranschaulicht, wie das Nullobjekt zu einem komplizierteren Muster wird:

#include <iostream>

class Animal {
 public:
  virtual ~Animal() = default;

  virtual void MakeSound() const = 0;
};

class Dog : public Animal {
 public:
  virtual void MakeSound() const override { std::cout << "woof!" << std::endl; }
};

class NullAnimal : public Animal {
 public:
  virtual void MakeSound() const override {}
};

Hier besteht die Idee darin, dass es Situationen gibt, in denen ein Zeiger oder eine Referenz auf ein AnimalObjekt erforderlich ist, aber kein geeignetes Objekt verfügbar ist. Eine Nullreferenz ist in standardkonformem C++ unmöglich. Ein Null- Animal*Zeiger ist möglich und könnte als Platzhalter nützlich sein, darf aber nicht für den direkten Versand verwendet werden: a->MakeSound()ist undefiniertes Verhalten, wenn aes sich um einen Null-Zeiger handelt.

Das Nullobjektmuster löst dieses Problem, indem es eine spezielle NullAnimalKlasse bereitstellt, die an einen AnimalZeiger oder eine Referenz gebunden instanziiert werden kann .

Die spezielle Nullklasse muss für jede Klassenhierarchie erstellt werden, die ein Nullobjekt haben soll, da a NullAnimalnutzlos ist, wenn ein Nullobjekt in Bezug auf eine WidgetBasisklasse benötigt wird , die nicht mit der AnimalHierarchie in Beziehung steht .

Beachten Sie, dass es ein wichtiges Merkmal ist, überhaupt keine Nullklasse zu haben, im Gegensatz zu Sprachen, in denen "alles eine Referenz" ist (zB Java und C#). In C++ kann der Entwurf einer Funktion oder Methode explizit angeben, ob null zulässig ist oder nicht.

// Function which requires an |Animal| instance, and will not accept null.
void DoSomething(const Animal& animal) {
  // |animal| may never be null here.
}

// Function which may accept an |Animal| instance or null.
void DoSomething(const Animal* animal) {
  // |animal| may be null.
}

C#

C# ist eine Sprache, in der das Nullobjektmuster richtig implementiert werden kann. Dieses Beispiel zeigt Tierobjekte, die Sounds anzeigen, und eine NullAnimal-Instanz, die anstelle des C#-Null-Schlüsselworts verwendet wird. Das null-Objekt bietet konsistentes Verhalten und verhindert eine Laufzeit-NULL-Verweisausnahme, die auftreten würde, wenn stattdessen das null-Schlüsselwort von C# verwendet würde.

/* Null object pattern implementation:
 */
using System;

// Animal interface is the key to compatibility for Animal implementations below.
interface IAnimal
{
	void MakeSound();
}

// Animal is the base case.
abstract class Animal : IAnimal
{
	// A shared instance that can be used for comparisons
	public static readonly IAnimal Null = new NullAnimal();
	
	// The Null Case: this NullAnimal class should be used in place of C# null keyword.
	private class NullAnimal : Animal
	{
		public override void MakeSound()
		{
			// Purposefully provides no behaviour.
		}
	}
	public abstract void MakeSound();
}

// Dog is a real animal.
class Dog : Animal
{
	public override void MakeSound()
	{
		Console.WriteLine("Woof!");
	}
}

/* =========================
 * Simplistic usage example in a Main entry point.
 */
static class Program
{
	static void Main()
	{
		IAnimal dog = new Dog();
		dog.MakeSound(); // outputs "Woof!"

		/* Instead of using C# null, use the Animal.Null instance.
         * This example is simplistic but conveys the idea that if the Animal.Null instance is used then the program
         * will never experience a .NET System.NullReferenceException at runtime, unlike if C# null were used.
         */
		IAnimal unknown = Animal.Null;  //<< replaces: IAnimal unknown = null;
		unknown.MakeSound(); // outputs nothing, but does not throw a runtime exception        
	}
}

Smalltalk

Nach dem Smalltalk-Prinzip ist alles ein Objekt , die Abwesenheit eines Objekts wird selbst durch ein Objekt, genannt , modelliert nil. In dem GNU Smalltalk zum Beispiel aus der Klasse nilist UndefinedObject, ein direkter Nachkomme von Object.

Jede Operation, die für ihren Zweck kein sinnvolles Objekt zurückgibt, kann nilstattdessen zurückkehren, wodurch der Sonderfall der Rückgabe von "kein Objekt" vermieden wird, das von Smalltalk-Designern nicht unterstützt wird. Dieses Verfahren hat den Vorteil der Einfachheit (kein Spezialfall erforderlich) gegenüber dem klassischen Ansatz "Null" oder "Kein Objekt" oder "Null-Referenz". Besonders nützliche Nachrichten, die mit verwendet nilwerden können isNil, sind , ifNil:oder ifNotNil:, die den Umgang mit möglichen Verweisen auf nilin Smalltalk-Programmen praktisch und sicher machen .

Gemeinsame Lisp

In Lisp können Funktionen das spezielle Objekt problemlos akzeptieren nil, wodurch die Anzahl der Sonderfalltests im Anwendungscode reduziert wird. Obwohl niles zum Beispiel ein Atom ist und keine Felder hat, akzeptieren die Funktionen carund es und geben es einfach zurück, was sehr nützlich ist und zu kürzerem Code führt. cdrnil

Da nil es sich bei Lisp um die leere Liste handelt, existiert die oben in der Einleitung beschriebene Situation nicht. Code, der zurückgibt, nilgibt zurück, was tatsächlich die leere Liste ist (und nichts, das einem NULL-Verweis auf einen Listentyp ähnelt), sodass der Aufrufer den Wert nicht testen muss, um zu sehen, ob er eine Liste enthält oder nicht.

Das Nullobjektmuster wird auch in der Mehrfachwertverarbeitung unterstützt. Wenn das Programm versucht, einen Wert aus einem Ausdruck zu extrahieren, der keine Werte zurückgibt, wird das Nullobjekt nilersetzt. Somit wird (list (values))zurückgegeben (nil)(eine einelementige Liste, die nil enthält). Der (values)Ausdruck gibt überhaupt keine Werte zurück, aber da der Funktionsaufruf von listseinen Argumentausdruck auf einen Wert reduzieren muss, wird das Nullobjekt automatisch ersetzt.

SCHLIESSEN

In Common Lisp ist das Objekt nildie einzige Instanz der Sonderklasse null. Dies bedeutet, dass eine Methode auf die nullKlasse spezialisiert werden kann , wodurch das Null-Entwurfsmuster implementiert wird. Das heißt, es ist im Wesentlichen in das Objektsystem eingebaut:

;; empty dog class

(defclass dog () ())

;; a dog object makes a sound by barking: woof! is printed on standard output
;; when (make-sound x) is called, if x is an instance of the dog class.

(defmethod make-sound ((obj dog))
  (format t "woof!~%"))

;; allow (make-sound nil) to work via specialization to null class.
;; innocuous empty body: nil makes no sound.
(defmethod make-sound ((obj null)))

Die Klasse nullist eine Unterklasse der symbolKlasse, da niles sich um ein Symbol handelt. Da nilauch die leere Liste repräsentiert, nullist auch eine Unterklasse der listKlasse. Methodenparameter, die auf ein Argument spezialisiert sind symboloder listannehmen nil. Natürlich nullkann noch eine Spezialisierung definiert werden, die spezieller für nil.

Planen

Im Gegensatz zu Common Lisp und vielen Dialekten von Lisp hat der Scheme-Dialekt keinen Nullwert, was auf diese Weise funktioniert; die Funktionen carund cdrdürfen nicht auf eine leere Liste angewendet werden; Scheme-Anwendungscode muss daher die empty?oder pair?Prädikatfunktionen verwenden, um diese Situation zu umgehen, selbst in Situationen, in denen sehr ähnliche Lisp dank des Verhaltens von nicht zwischen leeren und nicht leeren Fällen unterscheiden müsste nil.

Rubin

In Duck-Typ- Sprachen wie Ruby ist keine Sprachvererbung erforderlich, um das erwartete Verhalten bereitzustellen.

class Dog
  def sound
    "bark"
  end
end
 
class NilAnimal
  def sound(*); end
end

def get_animal(animal=NilAnimal.new)
  animal
end

get_animal(Dog.new).sound
 => "bark"
get_animal.sound
 => nil

Versuche, NilClass direkt mit Monkey-Patches zu patchen, anstatt explizite Implementierungen bereitzustellen, bringen mehr unerwartete Nebenwirkungen als Vorteile.

JavaScript

In Duck-Typ- Sprachen wie JavaScript ist keine Sprachvererbung erforderlich, um das erwartete Verhalten bereitzustellen.

class Dog {
  sound() {
    return 'bark';
  }
}

class NullAnimal {
  sound() {
    return null;
  }
}

function getAnimal(type) {
  return type === 'dog' ? new Dog() : new NullAnimal();
}

['dog', null].map((animal) => getAnimal(animal).sound());
// Returns ["bark", null]

Java

public interface Animal {
	void makeSound() ;
}

public class Dog implements Animal {
	public void makeSound() {
		System.out.println("woof!");
	}
}

public class NullAnimal implements Animal {
	public void makeSound() {
                // silence...
	}
}

Dieser Code veranschaulicht eine Variation des obigen C++-Beispiels unter Verwendung der Java-Sprache. Wie bei C++ kann eine NULL-Klasse in Situationen instanziiert werden, in denen ein Verweis auf ein AnimalObjekt erforderlich ist, aber kein geeignetes Objekt verfügbar ist. Ein Nullobjekt Animalist möglich ( Animal myAnimal = null;) und könnte als Platzhalter nützlich sein, darf aber nicht zum Aufrufen einer Methode verwendet werden. In diesem Beispiel myAnimal.makeSound();wird eine NullPointerException ausgelöst. Daher kann zusätzlicher Code erforderlich sein, um auf Nullobjekte zu testen.

Das Nullobjektmuster löst dieses Problem, indem es eine spezielle NullAnimalKlasse bereitstellt, die als Objekt vom Typ instanziiert werden kann Animal. Wie bei C++ und verwandten Sprachen muss diese spezielle Nullklasse für jede Klassenhierarchie erstellt werden, die ein Nullobjekt benötigt, da a NullAnimalnutzlos ist, wenn ein Nullobjekt benötigt wird, das die AnimalSchnittstelle nicht implementiert .

PHP

interface Animal
{
    public function makeSound();
}

class Dog implements Animal
{
    public function makeSound()
    { 
        echo "Woof.."; 
    }
}

class Cat implements Animal
{
    public function makeSound()
    { 
        echo "Meowww.."; 
    }
}

class NullAnimal implements Animal
{
    public function makeSound()
    { 
        // silence...
    }
}

$animalType = 'elephant';
switch($animalType) {
    case 'dog':
        $animal = new Dog();
        break;
    case 'cat':
        $animal = new Cat();
        break;
    default:
        $animal = new NullAnimal();
        break;
}
$animal->makeSound(); // ..the null animal makes no sound

Visual Basic .NET

Die folgende NULL-Objektmusterimplementierung demonstriert die konkrete Klasse, die ihr entsprechendes NULL-Objekt in einem statischen Feld bereitstellt Empty. Dieser Ansatz wird häufig in .NET Framework ( String.Empty, EventArgs.Empty, Guid.Empty, etc.) verwendet.

Public Class Animal
    Public Shared ReadOnly Empty As Animal = New AnimalEmpty()

    Public Overridable Sub MakeSound()
        Console.WriteLine("Woof!")
    End Sub
End Class

Friend NotInheritable Class AnimalEmpty
    Inherits Animal

    Public Overrides Sub MakeSound()
        ' 
    End Sub
End Class

Kritik

Dieses Muster sollte mit Bedacht verwendet werden, da Fehler/Bugs als normale Programmausführung erscheinen können.

Es sollte darauf geachtet werden, dieses Muster nicht zu implementieren, nur um Nullprüfungen zu vermeiden und den Code lesbarer zu machen, da der schwerer zu lesende Code möglicherweise an eine andere Stelle verschoben wird und weniger Standard ist – beispielsweise wenn eine andere Logik ausgeführt werden muss, falls das Objekt bereitgestellt ist in der Tat das Null-Objekt. Das gängige Muster in den meisten Sprachen mit Referenztypen besteht darin, eine Referenz mit einem einzelnen Wert zu vergleichen, der als null oder nil bezeichnet wird. Außerdem muss getestet werden, dass nirgendwo im Code jemals null anstelle des null-Objekts zugewiesen wird, da dies in den meisten Fällen und Sprachen mit statischer Typisierung kein Compilerfehler ist, wenn das null-Objekt einen Referenztyp hat, obwohl dies der Fall wäre führen sicherlich zu Fehlern zur Laufzeit in Teilen des Codes, in denen das Muster verwendet wurde, um Nullprüfungen zu vermeiden. Darüber hinaus wird in den meisten Sprachen und unter der Annahme, dass es viele Null-Objekte geben kann (dh das Null-Objekt ist ein Referenztyp, aber das Singleton-Muster nicht auf die eine oder andere Weise implementiert ), nach dem Null-Objekt statt nach dem gesucht null- oder nil-Werte verursachen Overhead, ebenso wie das Singleton-Muster wahrscheinlich selbst beim Abrufen der Singleton-Referenz.

Siehe auch

Verweise

Externe Links