Üres objektum programtervezési minta

A Wikipédiából, a szabad enciklopédiából
(Null Object (tervezési minta) szócikkből átirányítva)

Az objektumorientált számítógépprogramozásban az üres objektum egy objektum, amelynek a viselkedése semleges, ún. „null”. Az üres objektum programtervezési minta meghatározza az ilyen objektumok használatának módját és viselkedését (vagy annak hiányát). Először a Pattern Languages of Program Design könyvben jelent meg.[1]

Létrehozásának oka[szerkesztés]

A legtöbb objektumorientált nyelvben, pl. Java-ban vagy C#-ban a referenciák lehetnek null tartalmúak. Ezeket a referenciákat mindig ellenőrizni kell, hogy nem null-e az értékük, mivel a metódusok tipikusan nem hívhatóak meg null referenciára. A .Net keretrendszerben gyakran használt megoldás, hogy az osztályok tartalmaznak üres objektumot osztálymezőként.

Az Objective-C nyelv a null referenciát semleges működésű objektumként kezeli. A visszatérési érték nil, ha objektum, 0 ha szám, NO ha logikai érték, vagy egy struct, aminek minden tagja az előzőek szerint van inicializálva.[2]

A probléma megoldható másként is. Egyes nyelvek, mint a Smalltalk a null referenciát is objektumnak tekintik, a nyelvben minden objektum elve alapján. Egyes Lisp dialektusokban, mint Common Lisp, Scheme vagy a CLOS, a nil objektum éppen az üres lista. Kacsa típusozású nyelvekben a null referencia nyújthatja azt a viselkedést, amit az üres objektumtól elvárnánk.

Leírás[szerkesztés]

Ahelyett, hogy null referenciát adnánk vissza egy objektum hiányában (pl. egy nem létező vásárló), egy objektumot használunk, mely implementálja a megfelelő interfészt, de a metódus törzse üres. Ha visszatérési érték kell, az a megfelelő típus nulla értéke (null objektum, nulla, hamis). Ennek a megoldásnak az előnye, hogy az üres objektum nagyon kiszámítható és nincsen mellékhatása: egyszerűen nem csinál semmit.

Például, egy függvény visszaadhat egy listát egy mappában található fájlokról, hogy később valamilyen műveletet tudjunk végezni rajtuk. Üres mappa esetében megoldás lehet az, ha kivételt dobunk, vagy null referenciával térünk vissza. Így a kódrészletnek, ami egy listát vár ellenőriznie kell, hogy valóban listát kapott-e, ami bonyolítja a kódot.

Azzal, hogy üres objektumot adunk vissza (pl. egy üres listát) már nem kell törődnünk az ellenőrzéssel, egyszerűen csak végigiterálhatunk a listán. Azt is meg lehet állapítani, ha a visszatérési érték egy null objektum (pl. egy üres lista) és szükség esetén másképpen reagálni rá. Az üres objektum programtervezési mintát úgy is lehet használni, hogy egy csonkként működjön, ha esetleg még nincs kész egy bizonyos funkció, pl. egy adatbázis nem elérhető a tesztelés alatt.

Kapcsolat más mintákkal[szerkesztés]

Az állapot és a stratégia speciális esetének tekinthető.

Gammáék könyvében nem szerepel, de Martin Fowler: Refactoring[3] és Joshua Kerievsky refaktorálásról szóló könyve tartalmazza.[4]

Robert Cecil Martin: Agile Software Development: Principles, Patterns and Practices 17. fejezete foglalkozik a mintával.[5]

Kritika[szerkesztés]

Az üres objektum használatával vigyázni kell, mivel a hibás végrehajtás úgy tűnhet, mintha rendben lenne.

Az üres objektumnak egykének kell lennie, különben minden összehasonlítás lelassul, ahol ellenőrizzük, hogy valami nem üres objektum-e.

Mivel a null pointer még mindig jelen van a nyelvben, a tesztelésnek ellenőriznie kell, hogy nem fordul-e mégis elő a kódban. A kódban maradt null pointerek nem okoznak fordítási hibát, ellenben futás idejű hibát igen.

Az üres objektum nem használható csak az olvashatóság javítására, mivel nem hagyható el a rosszabbul olvasható rész, csak áthelyezhető.

Alternatívák[szerkesztés]

Null-kondicionális operátor[szerkesztés]

A C# 6.0-tól kezdve használható a ?. null-kondicionális operátor:

// 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();
        }
    }
}
// Az eredmény:
// 4

Kiterjesztési metódusok[szerkesztés]

Egyes .NET nyelvekben a kiterjesztési metódusok is betölthetik az üres objektum szerepét. A kiterjesztési metódusok osztálymetódusok, ám hívhatók null értékekre, mint példány metódusok. Ezek a metódusok ellenőrizhetik a null értéket, így az őket hívó függvényeknek már nem kell ezt megtenniük. Az alábbi kód a C# Null coalescing operátort használja, hogy garantálja a hibamentes hívást.

// 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());
        }
    }
}
// Az eredmény
// 18

Példa[szerkesztés]

Adott egy bináris fa ezzel az elem struktúrával:

class node {
  node left
  node right
}

Implementálhatjuk a fa méret függvényt rekurzívan:

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

Mivel a gyermek csomópontok nem biztos, hogy léteznek ezért a fa méret függvényünket úgy kell módosítani, hogy a gyermek csomópontok meglétét ellenőrizze.

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
}

Ez viszont a függvényt sokkal bonyolultabbá teszi. Az üres objektum programtervezési minta használatával készíthetünk egy speciális fa méret függvényt, ami csak a null csomópontokkal foglalkozik.

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

Ez elválasztja a sima logikát a speciális esetkezeléstől és egyszerűbbé teszi a kód megértését.

C++ példa[szerkesztés]

Statikusan típusozott objektumreferenciákkal illusztrálható, hogyan válik az üres objektum bonyolultabb mintává:

class animal {
public:
  virtual void make_sound() = 0;
};

class dog : public animal {
  void make_sound() { cout << "woof!" << endl; }
};

class null_animal : public animal {
  void make_sound() { }
};

Az alapötlet az, hogy ha mutató vagy hivatkozás elérhető egy Animal objektumra, de nincs meghatározva objektum, akkor null hivatkozás helyett ez hivatkozható. A szabvány C++-ban a null animal * lehetséges. Ám ez csak helykitöltésre alkalmas, hiszen a rajta hívott műveletek eredménye nem definiált: az a->make_sound() eredménye bármi lehet.

Az üres objektum minta külön osztályt deklarál erre a célra, ami lévén gyerek osztály a belőle definiált objektum hivatkozható animal pointerrel. A null osztályt minden osztályra külön létre kell hozni és példányosítani, mivel a null_animal osztálynak semmi köze például a widgetekhez.

Egy függvény vagy metódus explicit jelezheti, hogy meghívható-e nullra, vagy sem.

// Egy animal példány referenciáját váró metódus, ,
// ami nem lehet null
void do_something( const animal& Inst ) {
  // Az Inst itt biztos nem null
}

// függvény, ami elfogad animal példányt vagy nullt
void do_something( const animal* pInst ) {
  // pInst lehet null
}

C# példa[szerkesztés]

C# nyelven az üres objektum minta megfelelően implementálható. Ez a példa Állat objektumokat készít és a NullAnimal objektumot használja a C# null kulcsszava helyett. Az üres objektum egységes működést eredményez és megelőzi a futásidejű Null Reference Exception létrejöttét, ami akkor jönne létre, ha a C# null kulcsszavát használnák.

/* Null objektum mintaimplementáció:
 */
using System;
 
// Az Animal interfész a kulcs a különböző Animal implementációk kompatibilitásához.
interface IAnimal
{
    void MakeSound();
}
 
// A kutya egy igazi állat.
class Dog : IAnimal
{
    public void MakeSound()
    {
        Console.WriteLine("Woof!");
    }
}
 
// A Null eset: ez a NullAnimal osztály példányosítható és használható a C# null kulcsszava helyett.
class NullAnimal : IAnimal
{
    public void MakeSound()
    {
        // Céltudatosan nem csinál semmit.
    }
}
 
/* =========================
 * Egyszerű használatra példa
 */
static class Program
{
    static void Main()
    {
        IAnimal dog = new Dog();
        dog.MakeSound(); // Kimenet: "Woof!"
 
        /* A C# null helyett NullAnimal példányt használunk.
         * Ez a példa egyszerű, de közvetíti az ötletet miszerint, ha a NullAnimal példányt használjuk, akkor a program
         * sosem fut .NET System.NullReferenceException kivételre futásidőben, mintha a C# null kulcsszavát használtuk volna.
         */
        IAnimal unknown = new NullAnimal(); //<< helyettesítés: IAnimal unknown = null;
        unknown.MakeSound(); // a kimenet semmi, de legalább nem dob futásidejű kivételt.
    }
}

Smalltalk[szerkesztés]

A Smalltalkban minden objektum. Még azt is objektum modellezi, hogy egy objektum nem létezik. A nem létező objektum a nil, aminek osztálya a GNU Smalltalkban az UndefinedObject, ami az Object gyereke.

Minden művelet, ami nem tud megfelelő objektumot visszaadni, a nil objektummal válaszol. Ennek előnye az egyszerűség, mivel nem igényel esetszétválasztást. Különösen hasznos üzenetek az isNil és az ifNil; ezekkel praktikusan és biztonságosan kezelhetők a nil objektumoknak küldött üzenetek.

Common Lisp[szerkesztés]

A Common Lisp követi azt a Lisp hagyományt, hogy a nil azonos az üres listával. A függvények elfogadhatják paraméterként, például a car (fej) és a cdr (farok) függvények is elfogadják, és nilt adnak vissza. Ez nagyon hasznos, és lerövidíti a kódot.

Mivel a nil éppen az üres lista, a bevezetőben leírt probléma nem fordulhat elő. A nil nem referencia, ezért a hívónak nem kell tesztelnie, hogy nilt kapott-e.

A több értékű feldolgozás is támogatja az üres objektum mintát. Ha a program egy olyan kifejezésből akar kivenni értékeket, amiben nincsenek értékek, akkor a nil null objektum helyettesítődik be. Így a (list (values)) a (nil) listával tér vissza, aminek egyetlen eleme az üres lista.

Common Lispben a nil a null osztály egyetlen példánya. Ez azt jelenti, hogy egy metódus specializálható a null osztályra, hogy alkalmazzuk rá az üres objektum mintát.

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

A null osztály a symbol osztály alosztálya, mivel a nil szimbólum. A null osztály a list alosztálya is, mivel a nil is lista; emiatt a szimbólumokat és a listákat kezelő függvényeknek fogadniuk kell a nilt paraméterként.

Scheme[szerkesztés]

Több más Lisptől eltérően a Scheme-ben másként viselkedik a nil. A car és a cdr függvények nem alkalmazhatók üres listára. Predikátumfüggvények döntenek arról, hogy nem üres-e a lista, és csak akkor csinálnak bármit is, ha a lista nem üres. Az empty? és a pair? függvényekkel lehet vizsgálni, hogy amit kaptunk, az üres-e. Többnyire azonban nem kell megkülönböztetni az üres és a nem üres eseteket a nil viselkedésének köszönhetően.

Ruby[szerkesztés]

A kacsa típusozású nyelvekben, mint a Ruby, az elvárt viselkedéshez nincs szükség öröklésre, megteszi az ad-hoc polimorfizmus is.

 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

A NilClass közvetlen elérése több hátránnyal jár, mint amennyire hasznos.

JavaScript[szerkesztés]

A JavaScript szintén kacsa típusozású:

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[szerkesztés]

Javában az üres objektum a C++-hoz hasonlóan hozható létre, ami az adott osztályhoz tartozó speciális null osztály példánya. Az egyik osztály üres objektuma nem használható a másik osztályhoz, külön-külön kell elkészíteni mindegyikhez. Javában a referenciák lehetnek nullok, (Animal myAnimal = null;) de csak helyfoglalásra jók, de bármelyik, az üres objektumra meghívott metódus NullPointerExceptiont dob, még az osztálymetódusok is. Az állandó tesztelgetésre az üres objektum nyújt megoldást.

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() {

	}
}

PHP[szerkesztés]

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[szerkesztés]

A .NET keretrendszerben szokás, hogy az osztály az üres objektumot az Empty mezőben tartalmazza, mint String.Empty, EventArgs.Empty, Guid.Empty, és a többi. Az alábbi Visual Basic implementáció ezt mutatja be:

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

Jegyzetek[szerkesztés]

  1. Woolf, Bobby. Pattern Languages of Program Design 3. Addison-Wesley (1998) 
  2. Working with Objects (Working with nil). iOS Developer Library . Apple, Inc., 2012. december 13. (Hozzáférés: 2014. május 19.)
  3. Fowler, Martin. Refactoring. Improving the Design of Existing Code. Addison-Wesley (1999). ISBN 0-201-48567-2 
  4. Kerievsky, Joshua. Refactoring To Patterns. Addison-Wesley (2004). ISBN 0-321-21335-1 
  5. Martin, Robert. Agile Software Development: Principles, Patterns and Practices. Pearson Education (2002). ISBN 0-13-597444-5 

Fordítás[szerkesztés]

Ez a szócikk részben vagy egészben a Null Object pattern című angol Wikipédia-szócikk fordításán alapul. Az eredeti cikk szerkesztőit annak laptörténete sorolja fel. Ez a jelzés csupán a megfogalmazás eredetét és a szerzői jogokat jelzi, nem szolgál a cikkben szereplő információk forrásmegjelöléseként.