Wzorzec obiektu zerowego — Null object pattern
W obiektowego programowania , o obiekt zerowy jest obiektem bez wskazanej wartości lub z określonym położeniu neutralnym ( „null”) zachowanie. Wzorzec projektowy obiektu zerowego opisuje zastosowania takich obiektów i ich zachowanie (lub ich brak). Po raz pierwszy została opublikowana w serii książek Pattern Languages of Program Design .
Motywacja
W większości języków zorientowanych obiektowo, takich jak Java lub C# , odwołania mogą być null . Te odwołania należy sprawdzić, aby upewnić się, że nie są null przed wywołaniem jakichkolwiek metod , ponieważ metody zazwyczaj nie mogą być wywoływane w przypadku odwołań o wartości null.
Język Objective-C przyjmuje inne podejście do tego problemu i nie robi nic podczas wysyłania wiadomości do nil; jeśli oczekiwana jest wartość zwracana, nil(dla obiektów), 0 (dla wartości liczbowych), NO(dla BOOLwartości) lub struktura (dla typów struktur) ze wszystkimi jej elementami członkowskimi zainicjowanymi do null/0/ NO/zwracana jest struktura inicjowana zerem.
Opis
Zamiast używać referencji null do przekazania braku obiektu (na przykład nieistniejącego klienta), używa się obiektu, który implementuje oczekiwany interfejs , ale którego treść metody jest pusta. Zaletą tego podejścia nad działającą domyślną implementacją jest to, że obiekt null jest bardzo przewidywalny i nie ma skutków ubocznych: nic nie robi .
Na przykład funkcja może pobrać listę plików w folderze i wykonać na każdym z nich jakąś akcję. W przypadku pustego folderu jedną odpowiedzią może być zgłoszenie wyjątku lub zwrócenie odwołania o wartości NULL zamiast listy. Tak więc kod, który oczekuje listy, musi zweryfikować, czy rzeczywiście ma taką listę, zanim przejdzie dalej, co może skomplikować projekt.
Zwracając w zamian pusty obiekt (tj. pustą listę), nie ma potrzeby sprawdzania, czy zwracana wartość jest w rzeczywistości listą. Funkcja wywołująca może po prostu normalnie iterować listę, skutecznie nic nie robiąc. Wciąż jednak można sprawdzić, czy zwracana wartość jest obiektem null (pustą listą) i w razie potrzeby reagować inaczej.
Wzorzec obiektu zerowego może być również używany do działania jako skrót do testowania, jeśli pewna funkcja, taka jak baza danych, nie jest dostępna do testowania.
Przykład
Biorąc pod uwagę drzewo binarne , o tej strukturze węzłów:
class node {
node left
node right
}
Można rekurencyjnie zaimplementować procedurę rozmiaru drzewa:
function tree_size(node) {
return 1 + tree_size(node.left) + tree_size(node.right)
}
Ponieważ węzły potomne mogą nie istnieć, należy zmodyfikować procedurę, dodając sprawdzenia nieistnienia lub null:
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
}
To jednak komplikuje procedurę przez mieszanie kontroli granic z normalną logiką i utrudnia jej odczytanie. Używając wzorca zerowego obiektu, można stworzyć specjalną wersję procedury, ale tylko dla pustych węzłów:
function tree_size(node) {
return 1 + tree_size(node.left) + tree_size(node.right)
}
function tree_size(null_node) {
return 0
}
Oddziela to normalną logikę od obsługi specjalnych przypadków i ułatwia zrozumienie kodu.
Związek z innymi wzorami
Można to uznać za szczególny przypadek wzorca State i wzorca Strategia .
Nie jest to wzorzec z Design Patterns , ale jest wymieniony w Refactoring Martina Fowlera i Refactoring To Patterns Joshua Kerievsky'ego jako refaktoryzacja wstawiania obiektu zerowego .
Rozdział 17 książki Roberta Cecila Martina Agile Software Development: Principles, Patterns and Practices poświęcony jest wzorcowi.
Alternatywy
Od C# 6.0 możliwe jest użycie "?." operator (aka operator warunkowy o wartości null ), który po prostu zwróci wartość null, jeśli jego lewy operand ma wartość null.
// 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
Metody rozszerzania i koalescencja zerowa
W niektórych językach Microsoft .NET metody rozszerzające mogą być używane do wykonywania tzw. „koalescencji zerowej”. Dzieje się tak, ponieważ metody rozszerzające mogą być wywoływane na wartościach null, tak jakby dotyczyło to "wywołania metody wystąpienia", podczas gdy w rzeczywistości metody rozszerzające są statyczne. Metody rozszerzające mogą być wykonane w celu sprawdzenia wartości null, zwalniając w ten sposób kod, który ich używa, z konieczności wykonywania tego. Należy zauważyć, że w poniższym przykładzie użyto operatora łączenia C# Null, aby zagwarantować bezbłędne wywoływanie, w którym mógłby również użyć bardziej przyziemnego, jeśli... wtedy... w przeciwnym razie. Poniższy przykład działa tylko wtedy, gdy nie obchodzi Cię istnienie null lub traktujesz tak samo null i pusty ciąg. Założenie może nie obowiązywać w innych aplikacjach.
// 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
W różnych językach
C++
Język ze statycznie typowanymi odniesieniami do obiektów ilustruje, jak obiekt pusty staje się bardziej skomplikowanym wzorcem:
#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 {}
};
Tutaj chodzi o to, że istnieją sytuacje, w których Animalwymagany jest wskaźnik lub odwołanie do obiektu, ale nie ma odpowiedniego dostępnego obiektu. Odwołanie o wartości NULL jest niemożliwe w C++ zgodnym ze standardem. Animal*Wskaźnik zerowy jest możliwy i może być użyteczny jako symbol zastępczy, ale nie może być używany do bezpośredniego wysyłania: a->MakeSound()jest niezdefiniowanym zachowaniem, jeśli ajest wskaźnikiem zerowym.
Wzorzec obiektu zerowego rozwiązuje ten problem, dostarczając specjalną NullAnimalklasę, którą można utworzyć powiązaną ze Animalwskaźnikiem lub referencją.
Specjalna klasa null musi zostać utworzona dla każdej hierarchii klas, która ma mieć obiekt zerowy, ponieważ a NullAnimaljest bezużyteczny, gdy potrzebny jest obiekt zerowy w odniesieniu do jakiejś Widgetklasy bazowej, która nie jest związana z Animalhierarchią.
Zauważ, że NIE posiadanie w ogóle klasy null jest ważną cechą, w przeciwieństwie do języków, w których "cokolwiek jest referencją" (np. Java i C#). W C++ projekt funkcji lub metody może jawnie określać, czy wartość null jest dozwolona, czy nie.
// 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.
}
DO#
C# to język, w którym można poprawnie zaimplementować wzorzec obiektu o wartości null. Ten przykład pokazuje obiekty zwierząt, które wyświetlają dźwięki, oraz wystąpienie NullAnimal używane zamiast słowa kluczowego C# null. Obiekt null zapewnia spójne zachowanie i zapobiega wyjątkowi odwołania o wartości null środowiska uruchomieniowego, który wystąpiłby, gdyby zamiast tego użyto słowa kluczowego null w języku C#.
/* 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
}
}
Pogawędka
Zgodnie z zasadą Smalltalka, wszystko jest obiektem , brak obiektu sam w sobie jest modelowany przez obiekt o nazwie nil. Na przykład w GNU Smalltalk klasa niljest UndefinedObject, bezpośredni potomek Object.
Każda operacja, która nie zwraca sensownego obiektu dla swojego celu, może nilzamiast tego zwrócić , unikając w ten sposób szczególnego przypadku zwracania „brak obiektu” nieobsługiwanego przez projektantów Smalltalk. Ta metoda ma zaletę prostoty (nie ma potrzeby stosowania specjalnego przypadku) w porównaniu z klasycznym podejściem „zerowym” lub „bez obiektu” lub „zerowym odwołaniem”. Szczególnie użyteczne komunikaty nilto isNil, ifNil:lub ifNotNil:, które sprawiają, że radzenie sobie z możliwymi odniesieniami nilw programach Smalltalk jest praktyczne i bezpieczne .
Wspólne seplenienie
W Lispie funkcje mogą wdzięcznie akceptować specjalny obiekt nil, co zmniejsza ilość testowania specjalnych przypadków w kodzie aplikacji. Na przykład, chociaż niljest atom i nie ma żadnych pól, funkcji cari cdrzaakceptować nili po prostu zwrócić go, co jest bardzo przydatne i wyniki w krótszym kodu.
Ponieważ nil jest to pusta lista w Lispie, sytuacja opisana we wstępie nie istnieje. Kod, który zwraca, zwraca nilto, co w rzeczywistości jest pustą listą (a nie niczego, co przypominałoby puste odwołanie do typu listy), więc wywołujący nie musi testować wartości, aby sprawdzić, czy ma ona listę.
Wzorzec obiektu zerowego jest również obsługiwany w przypadku przetwarzania wielu wartości. Jeśli program próbuje wyodrębnić wartość z wyrażenia, które nie zwraca żadnych wartości, zachowanie jest takie, że nilpodstawiony jest obiekt pusty . W ten sposób (list (values))zwraca (nil)(jednoelementowa lista zawierająca nil). (values)Wyrażenie zwraca żadnych wartości w ogóle, ale ponieważ wywołanie funkcji do listpotrzeb, aby zmniejszyć swój wyraz argument wartości, obiekt zerowy zostaje automatycznie zastąpiona.
ZAMKNIJ
W Common Lisp obiekt niljest jedyną instancją klasy special null. Oznacza to, że metoda może być wyspecjalizowana w nullklasie, implementując w ten sposób pusty wzorzec projektowy. Oznacza to, że jest on zasadniczo wbudowany w system obiektów:
;; 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)))
Klasa nulljest podklasą symbolklasy, ponieważ niljest symbolem. Ponieważ nilreprezentuje również pustą listę, nulljest również podklasą tej listklasy. Parametry metod wyspecjalizowane w symbollub w listzwiązku z tym przyjmą nilargument. Oczywiście nullnadal można zdefiniować specjalizację, która jest dokładniejszym dopasowaniem do nil.
Schemat
W przeciwieństwie do Common Lisp i wielu dialektów Lisp, dialekt Scheme nie ma wartości zero, która działa w ten sposób; funkcje cari cdrnie mogą być stosowane do pustej listy; Dlatego kod aplikacji schematu musi użyć funkcji empty?lub pair?predykatu, aby ominąć tę sytuację, nawet w sytuacjach, w których bardzo podobny Lisp nie musiałby rozróżniać pustych i niepustych przypadków dzięki zachowaniu nil.
Rubin
W językach kaczych, takich jak Ruby , dziedziczenie języka nie jest konieczne, aby zapewnić oczekiwane zachowanie.
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
Próby bezpośrednio małpy połączeniowych NilClass zamiast zapewniać wyraźne implementacje dać bardziej nieoczekiwane efekty uboczne niż korzyści.
JavaScript
W językach kaczych, takich jak JavaScript , dziedziczenie języka nie jest konieczne, aby zapewnić oczekiwane zachowanie.
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]
Jawa
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...
}
}
Ten kod ilustruje odmianę powyższego przykładu C++, wykorzystującą język Java. Podobnie jak w przypadku C++, klasę null można utworzyć w sytuacjach, w których Animalwymagane jest odwołanie do obiektu, ale nie ma odpowiedniego dostępnego obiektu. Obiekt o wartości null Animaljest możliwy ( Animal myAnimal = null;) i może być użyteczny jako symbol zastępczy, ale nie może być używany do wywoływania metody. W tym przykładzie myAnimal.makeSound();zgłosi NullPointerException. W związku z tym może być konieczny dodatkowy kod do testowania obiektów o wartości null.
Wzorzec obiektu null rozwiązuje ten problem, dostarczając specjalną NullAnimalklasę, którą można utworzyć jako obiekt typu Animal. Podobnie jak w przypadku C++ i pokrewnych języków, ta specjalna klasa null musi zostać utworzona dla każdej hierarchii klas, która potrzebuje obiektu null, ponieważ a NullAnimaljest bezużyteczne, gdy potrzebny jest obiekt null, który nie implementuje Animalinterfejsu.
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
Poniższa implementacja wzorca obiektu o wartości null demonstruje konkretną klasę udostępniającą odpowiadający mu obiekt o wartości null w polu statycznym Empty. Takie podejście jest często stosowane w Framework ( String.Empty, EventArgs.Empty, Guid.Empty, itd.).
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
Krytyka
Ten wzorzec powinien być używany ostrożnie, ponieważ może spowodować, że błędy/błędy pojawią się podczas normalnego wykonywania programu.
Należy uważać, aby nie zaimplementować tego wzorca tylko po to, aby uniknąć sprawdzania wartości null i uczynić kod bardziej czytelnym, ponieważ trudniejszy do odczytania kod może po prostu przenieść się w inne miejsce i być mniej standardowy — na przykład gdy inna logika musi zostać wykonana w przypadku obiektu pod warunkiem, że jest to rzeczywiście pusty obiekt. Powszechnym wzorcem w większości języków z typami referencyjnymi jest porównywanie referencji z pojedynczą wartością określaną jako null lub nil. Ponadto istnieje dodatkowa potrzeba sprawdzenia, czy żaden kod nigdzie nie przypisuje null zamiast obiektu null, ponieważ w większości przypadków i językach z typowaniem statycznym nie jest to błąd kompilatora, jeśli obiekt null jest typu referencyjnego, chociaż byłoby to z pewnością prowadzą do błędów w czasie wykonywania w częściach kodu, w których użyto wzorca, aby uniknąć sprawdzania wartości null. Ponadto, w większości języków i przy założeniu, że może istnieć wiele obiektów null (tj. obiekt null jest typem referencyjnym, ale nie implementuje wzorca singleton w taki czy inny sposób), sprawdzając obiekt null zamiast Wartość null lub zero wprowadza narzut, podobnie jak sam wzór singletona po uzyskaniu odniesienia singletona.