Inżynieria oprogramowania » Wzorce projektowe

NULL Object

Spis treści

     1 Informacje podstawowe
     2 Omówienie szczegółowe
          2.1 Wstęp
          2.2 Opis problemu
          2.3 Implementacja
          2.4 Koszty i Problemy
     3 Powiązane wzorce


Informacje podstawowe


Nazwa: Null Object
Klasyfikacja: Wzorzec refakoryzacyjny - Refactoring to Patterns - Joshua Kerievsky


Omówienie szczegółowe


Wstęp


Wyobraźmy sobie sytuację, w której korzystamy z obcego kodu. Każde wywołanie metody, która zwraca jakąś wartość musi zostać zabezpieczone dodatkowym kodem sprawdzającym czy zwrócona wartość nie jest pusta. Nasz kod "puchnie" w miarę korzystania z obcego API i w konsekwencji staje się coraz bardziej zagmatwany. Z czasem może okazać się, że taniej jest stworzyć własną bibliotekę niż korzystać z obcego kodu.
Wzorzec NULL Object pozwala na uniknięcie nam jako dostawcą biblioteki konieczności zmuszania klientów do tworzenia nadmiarowego kodu.

Opis problemu


Przyjrzyjmy się na poniższemu przykładowi.
class Something{
 
        public void doSth(Param param, Value addon){
                Value value = param.getValue();
                if(value != null){
                        Transaction trans = value.add(addon);
                        if(trans != null){
                                trans.commit();
                        }
                }
 
        }
 
}

Jest to bardzo częsty przypadek kodu wzbogaconego o niepotrzebne instrukcje if. W dodatku jeżeli przyjrzymy się dokładniej okazuje się, że parametr addon nie jest sprawdzany i przekazanie wartości null może (i zapewne spowoduje) błąd.
Przyjrzyjmy się innemu przypadkowi.
class Something{
 
        public void doSth(Param param){
                List list = param.getList();
                if(list!=null){
                        for(Element e : list){
                                e.call()
                        }                        
                }
 
        }
 
}

W tym przykładzie operując na liście sprawdzamy czy nie jest ona null. Dodatkowa instrukcja if tylko zaciemnia kod i powoduje, że staje się on mniej czytelny. Algorytm otrzymuje dodatkową ścieżkę wykonania, którą trzeba przetestować. To oznacza dodatkowy, zbędny, kod.

W obydwu tych przypadkach można zastosować wzorzec Null Object. Polega on na dostarczeniu implementacji, która "robi nic", a jednocześnie nie zwraca i nie jest wartością null.

Implementacja


Przykładową implementację w przypadku drugiego problemu stanowi znana z Javy Collections.EMPTY_LIST. Zwraca ona pustą, niemodyfikowalną listę, która pozwala na pominięcie dodatkowego kodu:
class Param{
 
   List getList(){
      if(condition()){
          return makeListFromData();
      }
      return Collections.EMPTY_LIST;
   }
 
}
 
class Something{
 
        public void doSth(Param param){
                List list = param.getList();
                for(Element e : list){
                    e.call()
                }                        
        }
 
}


Pierwszy przypadek jest trochę bardziej skomplikowany. Zazwyczaj Null Object dobrze sprawdza się w momencie gdy wywoływana metoda nic nie zwraca i nie modyfikuje parametrów. W przypadku gdy metoda zwraca pewien obiekt (z tej samej biblioteki) to jako programiści stajemy przed koniecznością implementacji wszystkich interfejsów w dodatkowej wersji. Początkowo może wydawać się, że takie rozwiązanie będzie czasochłonne, ale jeżeli komunikujemy się za pomocą interfejsów i nie ujawniamy klientowi żadnych informacji o swoich klasach to za pomocą IDE i automatycznego generowania kodu jest to dość szybki proces. Przykładowa implementacja Value i Transaction z pierwszego przykładu może wyglądać następująco:
interface Transaction{
 
        void commit();
 
        public static final Transaction NULL_TRANSACTION = new Transaction() {
 
                public void commit() {
 
                }
        }; 
 
}
 
interface Value{
 
        Transaction add(Value addon);
 
        public static final Value NULL_VALUE = new Value() {
 
                public Transaction add(Value addon) {
                        return Transaction.NULL_TRANSACTION;
                }
        };
 
}


Nie zawsze istnieje konieczność tworzenia własnych implementacji. Można wykorzystać już istniejące już elementy API na przykład puste kolekcje.

Koszty i Problemy


Główne koszty wprowadzenia tego wzorca są związane z koniecznością dostarczenia kompletu dodatkowych implementacji interfejsów. Wiąże się to z koniecznością stworzenia dedykowanych testów sprawdzających czy nasze puste obiekty na pewno "działają" poprawnie.
Najpoważniejszym problemem jest  implementacja metod, które modyfikują parametry wejściowe. Można to rozwiązać na dwa sposoby. Pierwszy to nie tworzenie metod modyfikujących. Zakładamy, że pracujemy na danych, ale ich nie zmieniamy. Jest to prostsza i bezpieczniejsza wersja ponieważ klient wie, że nie mieszamy mu w jego danych.
Drugi to zwracanie niezmodyfikowanego obiektu. W tym przypadku należy jasno powiedzieć klientowi, że możliwe jest iż jego dane nie zostały zmodyfikowane. Ten sposób jest dobrym rozwiązaniem wszędzie tam gdzie modyfikacja nie jest koniecznością, a tylko opcją nie mającą wpływu na dalsze przetwarzanie.

Powiązane wzorce


Może być obecny jako specyficzny przypadek we wzorcach
  1. Strategii
  2. Stan