Wzorzec Null Object - okładka

Wzorzec projektowy Null Object

Opublikowano Kategorie Czysty kodCzas czytania 5min

Null Object jest jednym z prostszych wzorców do zrozumienia i implementacji. W tym artykule poznasz specyfikę wzorca Null Object oraz dowiesz się, kiedy warto go wykorzystać. Przykłady kodu w artykule przygotowane zostały w TypeScripcie.

Ten wpis jest kolejnym wpisem z serii o wzorcach projektowych. Jeśli chcesz poznać inne wzorce projektowe lub dowiedzieć się czym są wzorce projektowe, to koniecznie sprawdź mój wpis o wzorcach projektowych.

Teoria i przykładowe zastosowanie Null Object

Null Object jest jednym ze wzorców behawioralnych. Znajdzie zastosowanie w miejscach, gdzie oczekujemy, że dany fragment kodu zwróci oczekiwany obiekt lub null (w środowisku JS/TS może to być również undefined). Do pokazania przykładowego problemu, który można rozwiązać tym wzorcem, przygotowałem fragment kodu.


const blogpostsRepository = new BlogpostsRepository( databaseDriver );
const imagesRepository = new ImagesRepository( s3Driver );

class Thumbnail {
  public constructor( public readonly url: string ) {}
}

class ThumbnailFactory {
  public constructor(
    private readonly _imagesRepository: ImagesRepository
  ) {}

  public async getThumbnail(
    postId: string,
    format: string
  ): Promise<Thumbnail | null> {
    const url = `posts/${ postId }/thumbnail.${ format }`;

    const exists = await this._imagesRepository.exists( url );

    return exists ? new Thumbnail( url ) : null;
  }
}

const thumbnailFactory = new ThumbnailFactory( imagesRepository );

async function loadBlogpost( blogpostId: string ): Promise<Blogpost> {
  const content: string = await blogpostsRepository.getContent( blogpostId );

  const thumbnail = await thumbnailFactory.getThumbnail( blogpostId, 'png' );

  if ( thumbnail ) {
    return new Blogpost( {
      content,
      thumbnailUrl: thumbnail.url
    } );
  }

  const defaultUrl = 'defaults/thumbnail.png';

  return new Blogpost( { content, thumbnailUrl: defaultUrl } );
}

Zadaniem kodu z przykładu jest ładowanie postów na blogu. Do załadowania posta konieczne jest załadowanie treści oraz thumbnaila. Na ich podstawie powstaje obiekt klasy Blogpost. Do stworzenia obiektu klasy Thumbnail wykorzystuję prostą Fabrykę. Treść posta otrzymujemy zawsze, jednak thumbnail jest opcjonalny. W przypadku braku thumbnaila ma zostać załadowany domyślny.

Przedstawiony kod jest napisany źle i jest to zrobione celowo. Jestem sobie w stanie wyobrazić, że taki kod mógł gdzieś powstać. Problemów w nim jest kilka:

  • Zduplikowany if — jeden na poziomie metody getThumbnail a drugi w metodzie loadBlogpost. W każdym z tych miejsc sprawdzamy tę samą rzecz tzn. czy thumbnail istnieje. Łamie to regułę DRY;
  • Obsługa wartości null zwracanej z fabryki. Każdorazowo chcąc odwołać się do thumbnail.url, konieczne jest sprawdzenie, czy thumbnail nie jest null-em;
  • Trzymanie szczegółów implementacyjnych dotyczących ładowania domyślnego thumbnaila poza ThumbnailFactory.

W refaktoryzacji tego kodu może pomóc właśnie Null Object. Wzorzec Null Object zakłada, że w sytuacji, gdy zwracana jest wartość pusta, zamiast zwracania pustej wartości można zwrócić Null Object. Jest to obiekt zastępczy klasy docelowej. Implementuje on ten sam interfejs, ale jego metody i właściwości mają puste/neutralne/domyślne zachowanie.

Idealnie wpasowuje się to w nasz przypadek. Refaktoryzację kodu sprowadziłem do stworzenia interfejsu IThumbnail definiującego strukturę thumbnaila. Następnie stworzyłem klasę DefaultThumbnail będącą Null Objectem. W przypadku, gdy w repozytorium nie znajdziemy miniaturki dla posta, zwracany jest Null Object z domyślnym URL-em.


const blogpostsRepository = new BlogpostsRepository( databaseDriver );
const imagesRepository = new ImagesRepository( s3Driver );

interface IThumbnail {
  url: string;
}

class Thumbnail implements IThumbnail {
  public constructor(
    public readonly url: string
  ) {}
}

class DefaultThumbnail implements IThumbnail {
  public readonly url = 'defaults/thumbnail.png';
}

class ThumbnailFactory {
  public constructor(
    private readonly _imagesRepository: ImagesRepository
    ) {}

  public async getThumbnail(
    postId: string,
    format: string
  ): Promise<IThumbnail> {
    const url = `posts/${ postId }/thumbnail.${ format }`;

    const exists = await this._imagesRepository.exists( url );

    return exists ? new Thumbnail( url ) : new DefaultThumbnail();
  }
}

const thumbnailFactory = new ThumbnailFactory( imagesRepository );

async function loadBlogpost( blogpostId: string ): Promise<Blogpost> {
  const content: string = blogpostsRepository.getContent( blogpostId );

  const thumbnail = await thumbnailFactory.getThumbnail( blogpostId, 'png' );

  return new Blogpost( { content, thumbnailUrl: thumbnail.url } );
}

W tym konkretnym przypadku zastosowanie wzorca rozwiązało wszystkie bolączki. Zduplikowany if zniknął, logika związana z ustawianiem URL-a jest w jednym miejscu oraz nie ma konieczności obsługi null-a poza fabryką.

Potencjalne problemy z Null Object

Korzystając z tego wzorca, warto być świadomym potencjalnych problemów i ograniczeń z nim związanych.

W przedstawionym przykładzie zadziałał on dobrze. Jednak sprawa mogłaby się skomplikować w przypadku, gdy domyślny obiekt miał być różny w zależności od warunków np. kategorii posta. Konieczne byłoby dodanie dodatkowej logiki albo w DefaultThumbnail lub w fabryce.

Problematyczne może być też tworzenie Null Objectów dla złożonych obiektów np. takich, które w poszczególnych metodach zwracają obiekty. Wtedy dla zwracanych obiektów również będzie trzeba stworzyć domyślne. Przykładowo, jeśli zamiast Thumbnail fabryka zwracałaby ThumbnailCatalog z różnymi rozmiarami miniaturek, konieczne byłoby stworzenie domyślnego obiektu dla każdego obsługiwanego rozmiaru.

Czasami też Null Object będzie zwyczajną armatą na muchę. W tym konkretnym przypadku uważam, że jest OK, bo enkapsuluje szczegóły dotyczące ścieżki do domyślnej miniatury. Jeśli jednak URL do miniatury byłby opcjonalny, znacznie prostszym rozwiązaniem byłoby skorzystanie z pierwszej wersji ThumbnailFactory i zrobienie czegoś takiego:


return new Blogpost( { content, thumbnailUrl: thumbnail?.url } );

Podsumowanie

Null Object to wzorzec prosty w wykorzystaniu i zrozumieniu. Jednak przed jego zastosowaniem warto odpowiedzieć sobie czy bardziej nas bolą dodatkowe if-y czy dodatkowe klasy. Daj znać czy go wykorzystujesz i czy znalazłeś/aś dla niego jakieś ciekawe zastosowanie.

Źródła i materiały dodatkowe

Dominik Szczepaniak

Zawodowo Senior Software Engineer w CKSource. Prywatnie bloger, fan włoskiej kuchni, miłośnik jazdy na rowerze i treningu siłowego.

Inne wpisy, które mogą Cię zainteresować

Zapisz się na mailing i odbierz e-booka

Odbierz darmowy egzemplarz e-booka 106 Pytań Rekrutacyjnych Junior JavaScript Developer i realnie zwiększ swoje szanse na rozmowie rekrutacyjnej! Będziesz też otrzymywać wartościowe treści i powiadomienia o nowych wpisach na skrzynkę e-mail.

Subscribe
Powiadom o
guest

0 komentarzy
oceniany
najnowszy najstarszy
Inline Feedbacks
View all comments