Wyjątki to wspaniały mechanizm, który pozwala zareagować na sytuacje, które uniemożliwiają funkcji wykonanie przeznaczonego jej zadania. Czasami jednak wyjątek, nie jest optymalnym wyjściem z sytuacji. Czasami istnieje lepsze rozwiązanie.
Poprawne dane pomimo sytuacji wyjątkowej
Jeśli wiemy, jak zostaną wykorzystane dane wynikowe zwrócone przez funkcje, możemy pomimo wystąpienia błędu, zwrócić dane, które nadal będą poprawne.
Spójrzmy na przykład z poprzedniego artykułu:
Cel istnienia funkcji get_wpdesk_post_title jest jasny, ma ona pobrać tytuł posta. Jeśli do jej odpowiedzialności dodamy poprawną reakcję na zaistnienie błędu, to możemy zmodyfikować jej zachowanie.
Co zyskaliśmy dzięki temu? W funkcji display_wpdesk_title już nie wystąpi wyjątek – stała się łatwiejsza w obsłudze. Końcowy kod, którego zadaniem jest wyświetlenie tytułu posta, jest jeszcze łatwiejszy – nie musi już brać pod uwagę sytuacji wyjątkowej.
Czy coś straciliśmy? Tak. Oddaliśmy decyzję, jak będzie wyglądał tytuł nieistniejącego posta funkcji display_wpdesk_title. To znaczy, że cały kod, który jest od niej zależny, nie będzie już mógł sam zadecydować jak postąpić w przypadku wystąpienia błędu. Rozwiązania, które w tym celu mogą próbować podejścia if ($result === 'Some placeholder title')
pominę milczeniem.
Jest jeszcze jeden potencjalny problem, który w tym momencie nie jest jeszcze wyraźnie widoczny. Scedowaliśmy do funkcji display_wpdesk_title reakcję na brak posta, ale w kodzie innego modułu np. odpowiadającego za wyświetlenie treści posta, inna funkcja będzie musiała zareagować na taką samą sytuację wyjątkową. Może się okazać, że sposób reakcji na ten problem będzie w przyszłości rozproszony po całym programie w różnych funkcjach i stracimy pojedyncze źródło prawdy. Nie będzie jednego miejsca, gdzie będzie można zmodyfikować zachowanie posta który nie istnieje. Reakcja na brak posta może też w pewnym momencie stać się niespójna, a nawet prowadzić do sprzecznych zachowań w programie.
Przykład ze świata obiektowego
Zanim spróbujemy ulepszyć kod jeszcze bardziej, zapomnijmy na chwilę o WordPress. Istnieje wzorzec projektowy, którego zadaniem jest rozwiązanie opisanego tutaj problemu. Jest to obiekt specjalnego przeznaczenia, tak zwany Pusty obiekt (ang. NullObject). Żeby go skutecznie zastosować, powinniśmy mieć możliwość implementacji interfejsu dla klasy, dla której chcemy dodać “puste zachowanie”.
Rozważmy następujący przypadek. Chcemy w naszym programie logować błędy. W tym celu chcielibyśmy utworzyć funkcję get_logger która w wyniku zwróci nam obiekt, którego zadaniem jest logowanie błędów w odpowiednim miejscu. Na przykład błędy krytyczne logowane są w pliku, ale poza tym wysyłane na kanał Slacka, a normalne błędy logowane są do pliku, a poza tym wysyłane mailem.
Funkcja get_logger zwraca obiekt, który implementuje interfejs LoggerInterface. To prosty polimorfizm. Użytkownik funkcji nie wie jaką implementację obiektu otrzyma i nie ma to dla niego znaczenia. Znaczenie ma pewność, którą daje nam interfejs, czyli, że obiekt będzie miał publiczne metody zadeklarowane w LoggerInterface. Znaczenie ma to, że wynikowy obiekt będzie umiał logować.
Dzięki takiemu podejściu rozwiązanie polega na abstrakcji czyli na pojęciu czym w istocie jest funkcjonalność logowania, a nie jak dokładnie została ona zaimplementowana.
Jeśli jest tak dobrze, to dlaczego jest tak źle?
Kod wygląda dobrze, wszystko ładnie się loguje, a na dedykowanym kanale Slacka pojawiają się ważniejsze problemy. Jak jednak wyglądałby ten kod, gdybyśmy pozwolili funkcji get_logger na zwrotkę nulla albo rzucanie wyjątków? Przecież logger jest dość skomplikowany. Slack może mieć problem z API, plik z logami może nie mieć uprawnień do zapisu, serwer SMTP może nie być dostępny. Jeśli dopuścimy sytuację, że get_logger rzuca wyjątek, albo – co gorsza – zwraca nulla, to ten prosty kod zmieni się w koszmar. Każde wywołanie get_logger trzeba będzie opakować w try/catch. Możemy oczywiście użyć zmiennej pomocniczej, ale najprawdopodobniej to nie jest jedyne miejsce w kodzie, gdzie będziemy używać loggera. Przecież nie chcemy go przekazywać jako parametru do każdej funkcji w programie.
Wzorzec projektowy NullObject na ratunek
Dlatego, w przypadku gdy klasa logująca nie może zostać poprawnie utworzona, wykorzystamy obiekt, który zaimplementuje pustą funkcjonalność.
Pięknie i prosto. Teraz jeśli get_logger ma problem, to reszta programu nie musi o tym wiedzieć. W takiej sytuacji nic nie zostanie zapisane do logów, ale program nadal będzie działał. Oczywiście warto jakoś zareagować na fakt, że nie mamy możliwości logowania, jednak to nie jest problem get_logger. Tym bardziej nie jest to zadanie funkcji display_wpdesk_title.
Wracamy do WordPress
Uzbrojeni w nową wiedzę możemy wrócić do pierwotnego problemu z obsługą błędów w tytule posta. Ponieważ jest to WordPress to mamy pewien problem.
WordPress nie jest napisany obiektowo, więc klasa WP_Post nie tylko nie implementuje żadnego interfejsu, który moglibyśmy rozszerzyć. Klasa nie rozszerza też innej klasy, a dodatkowo jest zadeklarowana ze słowem kluczowym final (co jest bardzo dobrym pomysłem). Niestety słowo kluczowe final powoduje, że nie możemy w żaden sposób rozszerzyć także WP_Post. Nie możemy więc skorzystać z polimorfizmu. Dlatego jeśli chcemy stworzyć WPNullPost, to musimy odwołać się do sprytu.
Dzięki takiemu rozwiązaniu udało się zminimalizować wspomniane wcześniej wady get_wpdesk_post_title. Teraz istnieje jedno miejsce, które gromadzi wiedzę o tym, jak reprezentujemy brakujący post, a kod jeszcze lepiej oddaje intencje.
Mam nadzieję, że artykuł trochę przybliżył problemy, które rozwiązujemy w ramach Review w WP Desk. Dajcie znać w komentarzach, czy chcecie wiedzieć jeszcze więcej o naszych praktykach pracy! :)