Model de obiect nul - Null object pattern

În programarea computerizată orientată pe obiecte , un obiect nul este un obiect fără valoare de referință sau cu un comportament neutru definit („nul”). Modelul de proiectare a obiectului nul descrie utilizările acestor obiecte și comportamentul lor (sau lipsa acestora). A fost publicat pentru prima dată în seria de cărți Limbaje de modelare a proiectării programelor .

Motivație

În majoritatea limbajelor orientate obiect, cum ar fi Java sau C # , referințele pot fi nule . Aceste referințe trebuie verificate pentru a ne asigura că nu sunt nule înainte de a invoca metode , deoarece metodele nu pot fi invocate în mod normal pe referințe nule.

Limbajul Objective-C are o altă abordare a acestei probleme și nu face nimic la trimiterea unui mesaj nil; dacă se așteaptă o valoare de returnare, nil(pentru obiecte), 0 (pentru valori numerice), NO(pentru BOOLvalori) sau o structură (pentru tipurile de structuri) cu toți membrii inițializați la null/ 0 / NO/ struct inițializată la zero este returnată.

Descriere

În loc să se utilizeze o referință nulă pentru a transmite absența unui obiect (de exemplu, un client inexistent), se folosește un obiect care implementează interfața așteptată , dar al cărui corp de metodă este gol. Avantajul acestei abordări față de o implementare implicită funcțională este că un obiect nul este foarte previzibil și nu are efecte secundare: nu face nimic .

De exemplu, o funcție poate prelua o listă de fișiere dintr-un folder și poate efectua unele acțiuni pe fiecare. În cazul unui folder gol, un răspuns poate fi aruncarea unei excepții sau returnarea unei referințe nule mai degrabă decât a unei liste. Astfel, codul care așteaptă o listă trebuie să verifice dacă are de fapt unul înainte de a continua, ceea ce poate complica proiectarea.

Prin returnarea unui obiect nul (adică o listă goală), nu este necesar să verificați dacă valoarea returnată este de fapt o listă. Funcția de apel poate pur și simplu itera lista ca normal, fără a efectua nimic. Cu toate acestea, este încă posibil să verificați dacă valoarea returnată este un obiect nul (o listă goală) și să reacționați diferit dacă doriți.

Modelul obiectului nul poate fi folosit și pentru a acționa ca un test pentru testare, dacă o anumită caracteristică, cum ar fi o bază de date, nu este disponibilă pentru testare.

Exemplu

Având în vedere un arbore binar , cu această structură de nod:

class node {
    node left
    node right
}

Se poate implementa recursiv o procedură de mărime a arborelui:

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

Deoarece este posibil ca nodurile copil să nu existe, trebuie să modificați procedura adăugând verificări inexistente sau nule:

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
}

Acest lucru, însă, face procedura mai complicată prin amestecarea verificărilor la graniță cu logica normală și devine mai greu de citit. Folosind modelul obiectului nul, se poate crea o versiune specială a procedurii, dar numai pentru nodurile nule:

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

Aceasta separă logica normală de tratarea cazurilor speciale și face codul mai ușor de înțeles.

Relația cu alte tipare

Poate fi privit ca un caz special al modelului de stat și al modelului de strategie .

Acesta nu este un model de modele de design , dar este menționat în Martin Fowler lui refactoring și Joshua Kerievsky lui Refactorizare la modele ca obiect Insert Null refactorizarii .

Capitolul 17 din Dezvoltarea software Agile a lui Robert Cecil Martin : Principii, tipare și practici este dedicat tiparului.

Alternative

Din C # 6.0 este posibil să folosiți „?”. operator (aka operator condițional nul ), care va evalua pur și simplu la nul dacă operandul său stâng este nul.

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

Metode de extindere și coalescență nulă

În unele limbaje Microsoft .NET , metodele de extensie pot fi utilizate pentru a efectua ceea ce se numește „coalescență nulă”. Acest lucru se datorează faptului că metodele de extensie pot fi apelate la valori nule ca și cum ar fi vorba despre o „invocare a metodei de instanță”, în timp ce metodele de extensie sunt statice. Metodele de extensie pot fi făcute pentru a verifica valorile nule, eliberând astfel codul care le folosește de faptul că trebuie să o facă vreodată. Rețineți că exemplul de mai jos folosește operatorul de coalescență C # Null pentru a garanta invocarea fără erori, unde ar fi putut folosi și un mod mai banal dacă ... atunci ... altfel. Următorul exemplu funcționează numai atunci când nu vă pasă de existența nulului sau când tratați șirul nul și golul la fel. Presupunerea nu poate fi valabilă în alte aplicații.

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

În diferite limbi

C ++

Un limbaj cu referințe la obiecte tastate static ilustrează modul în care obiectul nul devine un model mai complicat:

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

Aici, ideea este că există situații în care Animaleste necesar un indicator sau o referință la un obiect, dar nu există un obiect adecvat disponibil. O referință nulă este imposibilă în C ++ conform standardului. Un Animal*pointer nul este posibil și ar putea fi util ca suport de loc, dar nu poate fi utilizat pentru expediere directă: a->MakeSound()este un comportament nedefinit dacă aeste un pointer nul.

Modelul obiectului nul rezolvă această problemă oferind o NullAnimalclasă specială care poate fi instanțiată legată de un Animalpointer sau referință.

Clasa specială nulă trebuie creată pentru fiecare ierarhie de clase care urmează să aibă un obiect nul, deoarece NullAnimala nu este utilă atunci când este necesar un obiect nul în ceea ce privește o Widgetclasă de bază care nu are legătură cu Animalierarhia.

Rețineți că NU avea deloc o clasă nulă este o caracteristică importantă, spre deosebire de limbile în care „orice este o referință” (de exemplu, Java și C #). În C ++, proiectarea unei funcții sau metode poate specifica în mod explicit dacă este permis sau nu nul.

// 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 # este un limbaj în care modelul obiectului nul poate fi implementat corect. Acest exemplu prezintă obiecte de animale care afișează sunete și o instanță NullAnimal utilizată în locul cuvântului cheie C # null. Obiectul nul oferă un comportament consecvent și împiedică o excepție de referință nulă de execuție care ar avea loc dacă cuvântul cheie nul C # ar fi folosit în loc.

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

Convorbire scurtă

Urmând principiul Smalltalk, totul este un obiect , absența unui obiect este ea însăși modelată de un obiect, numit nil. În GNU Smalltalk, de exemplu, clasa lui nileste UndefinedObject, un descendent direct al Object.

Orice operație care nu returnează un obiect sensibil în scopul său poate reveni nilîn schimb, evitând astfel cazul special de a returna „niciun obiect” neacceptat de proiectanții Smalltalk. Această metodă are avantajul simplității (nu este nevoie de un caz special) față de abordarea clasică „nul” sau „fără obiect” sau „referință nulă”. Mesajele deosebit de utile pentru a fi utilizate nilsunt isNil, ifNil:sau ifNotNil:,, care fac practic și sigur tratarea posibilelor referințe la nilprogramele Smalltalk.

Lisp comun

În Lisp, funcțiile pot accepta cu grație obiectul special nil, ceea ce reduce cantitatea de testare specială a cazurilor din codul aplicației. De exemplu, cu toate că nileste un atom și nu are nici câmpuri, funcțiile carși să cdraccepte nilși doar întoarce, care este foarte util și rezultatele în cod mai scurt.

Deoarece nil este lista goală din Lisp, situația descrisă în introducerea de mai sus nu există. Codul care returnează nilreturnează ceea ce este de fapt lista goală (și nu ceva care seamănă cu o referință nulă la un tip de listă), astfel încât apelantul nu trebuie să testeze valoarea pentru a vedea dacă are sau nu o listă.

Modelul obiectului nul este, de asemenea, acceptat în procesarea valorilor multiple. Dacă programul încearcă să extragă o valoare dintr-o expresie care nu returnează valori, comportamentul este că obiectul nul nileste înlocuit. Astfel (list (values))se returnează (nil)(o listă cu un singur element care conține zero). (values)Expresia returneaza fara nici o valoare, dar din moment ce apelul funcției listtrebuie să își reducă expresia argument la o valoare, obiectul nul este substituit în mod automat.

ÎNCHIS

În Common Lisp, obiectul nileste singura instanță a clasei speciale null. Ceea ce înseamnă acest lucru este că o metodă poate fi specializată pentru nullclasă, implementând astfel modelul de proiectare nul. Ceea ce înseamnă că este în esență încorporat în sistemul de obiecte:

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

Clasa nulleste o subclasă a symbolclasei, deoarece nileste un simbol. Deoarece nilreprezintă și lista goală, nulleste și o subclasă a listclasei. Parametrii metodelor specializați symbolsau listvor lua astfel un nilargument. Desigur, o nullspecializare poate fi încă definită, care este o potrivire mai specifică pentru nil.

Sistem

Spre deosebire de Common Lisp și de multe dialecte ale Lisp, dialectul Scheme nu are o valoare nulă care funcționează astfel; funcțiile carși cdrnu pot fi aplicate unei liste goale; Prin urmare, codul aplicației de schemă trebuie să utilizeze funcțiile empty?sau pair?funcțiile predicate pentru a evita această situație, chiar și în situații în care Lisp foarte asemănător nu ar trebui să distingă cazurile goale și non-goale datorită comportamentului nil.

Rubin

În limbile tipate de rață, cum ar fi Ruby , moștenirea limbii nu este necesară pentru a oferi un comportament așteptat.

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

Încercările de direct maimuță-patch - uri NilClass în loc de a furniza implementări explicite a da efecte secundare mai neașteptate decât beneficii.

JavaScript

În limbile cu tip de rață, cum ar fi JavaScript , moștenirea limbii nu este necesară pentru a oferi un comportament scontat.

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

Acest cod ilustrează o variantă a exemplului C ++ de mai sus, utilizând limbajul Java. Ca și în cazul C ++, o clasă nulă poate fi instanțiată în situații în care Animaleste necesară o referință la un obiect, dar nu există un obiect adecvat disponibil. Un Animalobiect nul este posibil ( Animal myAnimal = null;) și ar putea fi util ca suport, dar nu poate fi folosit pentru apelarea unei metode. În acest exemplu, myAnimal.makeSound();va arunca o NullPointerException. Prin urmare, poate fi necesar un cod suplimentar pentru a testa obiectele nule.

Modelul de obiect nul rezolvă această problemă oferind o NullAnimalclasă specială care poate fi instanțiată ca obiect de tip Animal. Ca și în cazul C ++ și limbajelor conexe, acea clasă nulă specială trebuie creată pentru fiecare ierarhie de clase care are nevoie de un obiect nul, deoarece un NullAnimalnu are nici un folos atunci când este necesar un obiect nul care nu implementează Animalinterfața.

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

Următoarea implementare a modelului de obiect nul demonstrează clasa concretă furnizând obiectul nul corespunzător într-un câmp static Empty. Această abordare este frecvent utilizată în .NET Framework ( String.Empty, EventArgs.Empty, Guid.Emptyetc.).

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

Critică

Acest model trebuie utilizat cu atenție, deoarece poate face ca erorile / erorile să apară ca o execuție normală a programului.

Trebuie avut grijă să nu implementăm acest model doar pentru a evita verificările nule și pentru a face codul mai lizibil, deoarece codul mai greu de citit se poate muta într-un alt loc și poate fi mai puțin standard - cum ar fi atunci când trebuie executată o logică diferită în cazul în care obiectul cu condiția este într-adevăr obiectul nul. Modelul comun în majoritatea limbilor cu tipuri de referință este de a compara o referință la o singură valoare denumită nulă sau nulă. De asemenea, există o nevoie suplimentară de testare a faptului că niciun cod nicăieri nu atribuie vreodată nul în locul obiectului nul, deoarece în majoritatea cazurilor și limbajelor cu tastare statică, aceasta nu este o eroare a compilatorului dacă obiectul nul este de tip referință, deși cu siguranță duce la erori în timpul rulării în părți ale codului în care modelul a fost utilizat pentru a evita verificările nule. Mai mult decât atât, în majoritatea limbilor și presupunând că pot exista multe obiecte nule (adică obiectul nul este un tip de referință, dar nu implementează modelul singleton într-un fel sau altul), verificând obiectul nul în loc de valoarea nulă sau nulă introduce cheltuielile generale, la fel ca modelul singleton probabil la obținerea referinței singleton.

Vezi si

Referințe

linkuri externe