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 metodygetThumbnail
a drugi w metodzieloadBlogpost
. 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ę dothumbnail.url
, konieczne jest sprawdzenie, czy thumbnail nie jestnull
-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
- Null Object Pattern – Design Patterns
- Koddlo – Null Object (Pusty obiekt)
- Koziołek (4programmers) – NULL Object
- Cezary Walenciuk – Wzorce projektowe C#: Null Object
- Jarosław Stadnicki – Null object mi różnicy nie robi
- Refactoring Guru – Introduce Null Object
- Source Making – Null Object Design Pattern
- TypeScript docs – Type Guards and Differentiating Types
- Wzorzec projektowy Factory (Fabryka)
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.