Modello oggetto nullo - Null object pattern

Nella programmazione per computer orientata agli oggetti , un oggetto null è un oggetto senza alcun valore di riferimento o con un comportamento neutro ("null") definito. Il modello di progettazione dell'oggetto nullo descrive gli usi di tali oggetti e il loro comportamento (o la loro mancanza). È stato pubblicato per la prima volta nella serie di libri Pattern Languages ​​of Program Design .

Motivazione

Nella maggior parte dei linguaggi orientati agli oggetti, come Java o C# , i riferimenti possono essere null . Questi riferimenti devono essere controllati per assicurarsi che non siano null prima di richiamare metodi , perché i metodi in genere non possono essere richiamati su riferimenti null.

Il linguaggio Objective-C ha un altro approccio a questo problema e non fa nulla quando invia un messaggio a nil; se è previsto un valore restituito, nil(per oggetti), 0 (per valori numerici), NO(per BOOLvalori) o uno struct (per tipi struct) con tutti i suoi membri inizializzati su null/0/ NO/zero-struct inizializzato.

Descrizione

Invece di utilizzare un riferimento nullo per comunicare l'assenza di un oggetto (ad esempio, un cliente inesistente), si utilizza un oggetto che implementa l' interfaccia prevista , ma il cui corpo del metodo è vuoto. Il vantaggio di questo approccio rispetto a un'implementazione predefinita funzionante è che un oggetto null è molto prevedibile e non ha effetti collaterali: non fa nulla .

Ad esempio, una funzione può recuperare un elenco di file in una cartella ed eseguire alcune azioni su ciascuno di essi. Nel caso di una cartella vuota, una risposta potrebbe essere quella di generare un'eccezione o restituire un riferimento nullo anziché un elenco. Pertanto, il codice che si aspetta un elenco deve verificare che ne abbia effettivamente uno prima di continuare, il che può complicare la progettazione.

Restituendo invece un oggetto nullo (cioè un elenco vuoto), non è necessario verificare che il valore restituito sia effettivamente un elenco. La funzione chiamante può semplicemente iterare l'elenco normalmente, senza fare nulla. Tuttavia, è ancora possibile verificare se il valore restituito è un oggetto nullo (un elenco vuoto) e reagire in modo diverso se lo si desidera.

Il modello oggetto nullo può essere utilizzato anche come stub per il test, se una determinata funzionalità come un database non è disponibile per il test.

Esempio

Dato un albero binario , con questa struttura a nodi:

class node {
    node left
    node right
}

Si può implementare ricorsivamente una procedura di dimensione dell'albero:

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

Poiché i nodi figlio potrebbero non esistere, è necessario modificare la procedura aggiungendo controlli di inesistenza o nulli:

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
}

Ciò, tuttavia, rende la procedura più complicata mescolando i controlli dei limiti con la logica normale e diventa più difficile da leggere. Utilizzando il modello oggetto nullo, è possibile creare una versione speciale della procedura ma solo per i nodi null:

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

Ciò separa la logica normale dalla gestione dei casi speciali e semplifica la comprensione del codice.

Relazione con altri modelli

Può essere considerato un caso speciale del modello Stato e del modello Strategia .

Non è un modello da Design Patterns , ma è menzionato nel di Martin Fowler Refactoring Patterns e di Joshua Kerievsky refactoring come l'Inserisci oggetto Null refactoring .

Il capitolo 17 di Sviluppo software agile di Robert Cecil Martin : principi, modelli e pratiche è dedicato al modello.

alternative

Da C# 6.0 è possibile utilizzare il "?." operator (noto anche come operatore condizionale nullo ), che valuterà semplicemente come null se il suo operando sinistro è 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

Metodi di estensione e coalescenza Null

In alcuni linguaggi Microsoft .NET , i metodi di estensione possono essere utilizzati per eseguire ciò che viene chiamato 'null coalescing'. Questo perché i metodi di estensione possono essere chiamati su valori null come se si trattasse di una "chiamata del metodo di istanza" mentre in realtà i metodi di estensione sono statici. È possibile creare metodi di estensione per verificare la presenza di valori null, liberando così il codice che li utilizza dal doverlo fare. Si noti che l'esempio seguente usa l' operatore di coalescenza C# Null per garantire un'invocazione priva di errori, dove avrebbe anche potuto utilizzare un metodo if...then...else più banale. L'esempio seguente funziona solo quando non ti interessa l'esistenza di null o tratti allo stesso modo la stringa nulla e la stringa vuota. L'ipotesi potrebbe non reggere in altre applicazioni.

// 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 varie lingue

C++

Un linguaggio con riferimenti a oggetti tipizzati staticamente illustra come l'oggetto null diventa un modello più complicato:

#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 {}
};

Qui, l'idea è che ci sono situazioni in cui è richiesto un puntatore o un riferimento a un Animaloggetto, ma non è disponibile un oggetto appropriato. Un riferimento null è impossibile in C++ conforme allo standard. Un Animal*puntatore null è possibile e potrebbe essere utile come segnaposto, ma non può essere utilizzato per l'invio diretto: a->MakeSound()è un comportamento indefinito se aè un puntatore null.

Il modello di oggetto null risolve questo problema fornendo una NullAnimalclasse speciale che può essere istanziata associata a un Animalpuntatore o riferimento.

La classe null speciale deve essere creata per ogni gerarchia di classi che deve avere un oggetto null, poiché a NullAnimalnon serve quando ciò che serve è un oggetto null rispetto a una Widgetclasse base che non è correlata alla Animalgerarchia.

Nota che NON avere affatto una classe nulla è una caratteristica importante, a differenza dei linguaggi in cui "tutto è un riferimento" (ad esempio Java e C#). In C++, la progettazione di una funzione o di un metodo può indicare esplicitamente se null è consentito o meno.

// 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# è un linguaggio in cui il modello di oggetto null può essere implementato correttamente. Questo esempio mostra oggetti animali che visualizzano suoni e un'istanza NullAnimal usata al posto della parola chiave C# null. L'oggetto null fornisce un comportamento coerente e impedisce un'eccezione di riferimento null di runtime che si verificherebbe se si usasse invece la parola chiave C# null.

/* 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        
	}
}

chiacchiere

Seguendo il principio Smalltalk, tutto è un oggetto , l'assenza di un oggetto è essa stessa modellata da un oggetto, chiamato nil. In GNU Smalltalk, ad esempio, la classe di nilè UndefinedObject, un discendente diretto di Object.

Qualsiasi operazione che non riesce a restituire un oggetto sensibile per il suo scopo può nilinvece tornare , evitando così il caso speciale di restituire "nessun oggetto" non supportato dai designer di Smalltalk. Questo metodo ha il vantaggio della semplicità (non c'è bisogno di un caso speciale) rispetto al classico approccio "null" o "nessun oggetto" o "riferimento nullo". Messaggi particolarmente utili da utilizzare con nilsono isNil, ifNil:o ifNotNil:, che rendono pratico e sicuro gestire eventuali riferimenti a nilnei programmi Smalltalk.

Lispa comune

In Lisp, le funzioni possono accettare con grazia l'oggetto speciale nil, che riduce la quantità di test di casi speciali nel codice dell'applicazione. Ad esempio, sebbene nilsia un atomo e non abbia campi, le funzioni care cdraccettano nile semplicemente lo restituiscono, il che è molto utile e si traduce in un codice più breve.

Poiché nil è la lista vuota in Lisp, la situazione descritta nell'introduzione sopra non esiste. Il codice che restituisce nilrestituisce ciò che è in realtà l'elenco vuoto (e non qualcosa che assomigli a un riferimento null a un tipo di elenco), quindi il chiamante non ha bisogno di testare il valore per vedere se ha o meno un elenco.

Il modello oggetto nullo è supportato anche nell'elaborazione di più valori. Se il programma tenta di estrarre un valore da un'espressione che non restituisce alcun valore, il comportamento è che l'oggetto null nilviene sostituito. Quindi (list (values))restituisce (nil)(un elenco di un elemento contenente nil). L' (values)espressione non restituisce alcun valore, ma poiché la chiamata della funzione a listdeve ridurre l'espressione dell'argomento a un valore, l'oggetto null viene automaticamente sostituito.

CHIUDI

In Common Lisp, l'oggetto nilè l'unica istanza della classe speciale null. Ciò significa che un metodo può essere specializzato nella nullclasse, implementando in tal modo il modello di progettazione null. Vale a dire, è essenzialmente integrato nel sistema di oggetti:

;; 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)))

La classe nullè una sottoclasse della symbolclasse, perché nilè un simbolo. Poiché nilrappresenta anche la lista vuota, nullè anche una sottoclasse della listclasse. I parametri dei metodi sono specializzati symbolo listquindi accettano un nilargomento. Naturalmente, nullè ancora possibile definire una specializzazione che è una corrispondenza più specifica per nil.

schema

A differenza del Common Lisp e di molti dialetti del Lisp, il dialetto Scheme non ha un valore nullo che funziona in questo modo; le funzioni care cdrnon può essere applicato a una lista vuota; Il codice dell'applicazione Scheme deve quindi utilizzare le funzioni predicato empty?o pair?per aggirare questa situazione, anche in situazioni in cui Lisp molto simili non avrebbe bisogno di distinguere i casi vuoti e non vuoti grazie al comportamento di nil.

Rubino

Nei linguaggi anatra come Ruby , l'ereditarietà del linguaggio non è necessaria per fornire il comportamento previsto.

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

I tentativi di patchare direttamente NilClass invece di fornire implementazioni esplicite danno più effetti collaterali inaspettati che benefici.

JavaScript

Nei linguaggi anatra come JavaScript , l'ereditarietà del linguaggio non è necessaria per fornire il comportamento previsto.

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]

Giava

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...
	}
}

Questo codice illustra una variazione dell'esempio C++ sopra, utilizzando il linguaggio Java. Come con C++, è possibile creare un'istanza di una classe null in situazioni in cui Animalè richiesto un riferimento a un oggetto, ma non è disponibile alcun oggetto appropriato. Un Animaloggetto null è possibile ( Animal myAnimal = null;) e potrebbe essere utile come segnaposto, ma non può essere utilizzato per chiamare un metodo. In questo esempio, myAnimal.makeSound();genererà un'eccezione NullPointerException. Pertanto, potrebbe essere necessario codice aggiuntivo per verificare gli oggetti null.

Il modello di oggetto null risolve questo problema fornendo una NullAnimalclasse speciale che può essere istanziata come oggetto di tipo Animal. Come con C++ e linguaggi correlati, quella speciale classe null deve essere creata per ogni gerarchia di classi che necessita di un oggetto null, poiché a NullAnimalnon è utile quando è necessario un oggetto null che non implementa l' Animalinterfaccia.

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

L'implementazione del modello di oggetto null seguente dimostra la classe concreta che fornisce il suo oggetto null corrispondente in un campo statico Empty. Questo approccio viene spesso utilizzato in .NET Framework ( String.Empty, EventArgs.Empty, Guid.Empty, ecc.).

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

Critica

Questo modello dovrebbe essere usato con attenzione in quanto può far apparire errori/bug come una normale esecuzione del programma.

Bisogna fare attenzione a non implementare questo modello solo per evitare controlli nulli e rendere il codice più leggibile, poiché il codice più difficile da leggere può semplicemente spostarsi in un altro posto ed essere meno standard, come quando una logica diversa deve essere eseguita nel caso in cui l'oggetto fornito è effettivamente l'oggetto nullo. Il modello comune nella maggior parte dei linguaggi con tipi di riferimento consiste nel confrontare un riferimento con un singolo valore denominato null o nil. Inoltre, è necessario verificare che nessun codice da nessuna parte assegni mai null al posto dell'oggetto null, perché nella maggior parte dei casi e dei linguaggi con tipizzazione statica, questo non è un errore del compilatore se l'oggetto null è di un tipo di riferimento, anche se sarebbe certamente portare a errori in fase di esecuzione in parti del codice in cui è stato utilizzato il modello per evitare controlli null. Inoltre, nella maggior parte dei linguaggi e supponendo che possano esserci molti oggetti null (cioè, l'oggetto null è un tipo di riferimento ma non implementa il modello singleton in un modo o nell'altro), controllando l'oggetto null anziché il il valore null o nil introduce un sovraccarico, così come il modello singleton probabilmente stesso dopo aver ottenuto il riferimento singleton.

Guarda anche

Riferimenti

link esterno