Zagadka projektowa

Dziś będzie „lajcik”. Mam dla was zagadkę projektową. Załóżmy, że mamy zaprojektować klasę służącą do komunikacji z pewną usługą. Przy czym usługa ta odpowiada na zadane pytania w taki sposób, że odpowiedź może interesować wiele obiektów w ramach aplikacji. Przykładem może być tu proste wciśnięcie przycisku do którego możemy przypisać wiele listenerów. Generalnie zatem mówiąc mamy do czynienia ze wzorcem obserwatoraW. Jest on przydatny w wielu sytuacjach. Zadać by się mogło nawet niezbyt oczywistych. Na wzorcu tym można budować interfejsy SPI, ale też wykorzystać go do rozluźnienia powiązań pomiędzy obiektami. W tym ostatnim przypadku można zastąpić tradycyjne wiązanie jeden do jednego wiązaniem jeden do wielu dzięki czemu można z łatwością wpinać się w różne miejsca aplikacji bez potrzeby jej modyfikowania.

Przykładowo tradycyjna aplikacja wykorzystująca formularze CUD (Create, Uprade, Delete) ma zazwyczaj podpięty jeden obiekt DAO, który realizuje poszczególne operacje. Co jednak gdy chcemy informować różne usługi o zmianach zachodzących w obiekcie? Chociażby zapisywać logi do celów audytowych czy statystycznych? Oczywiście można wprowadzać zmiany. Ba! Można do tego użyć programowania aspektowego i wstawiać aspekty w runtime… można… można też od początku stosować kolekcje usług i później dodawać/usuwać pojedyncze elementy.

Jednak nie o tym chciałem, choć temat ciekawy.

Mamy sobie do zaimplementowania listener dla pewnej usługi. Wywołanie usługi może zakończyć się na dwa sposoby sukcesem albo porażką (LOL odkrywcze). Teraz zagadka.

Na poniższych listingach przedstawiono dwa interfejsy listenera pewnej usługi. Pytanie brzmi, który z nich jest lepszym rozwiązaniem i w jakiej sytuacji:

Listing 1. Listener „jednometodowy”

interface ServiceListener{
    public void onEvent(Event e);
}

Klasa Event zawiera odpowiedź usługi w czystej postaci (strumień, String, prymityw).

Listing 2. Listener „wielemetodowy”

interface ServiceListener{
    public void onSuccess(Response r);

    public void onError(Error e);
}

Klasy Response i Error zawierają odpowiedź w czystej postaci, ale są wstępnie filtrowane tak by rozróżnić sukcesy i porażki.

5 myśli na temat “Zagadka projektowa

  1. Listing 1. wyglada na listener-a, ktory jest wywolywany przed wywolaniem uslugi. Ten interfejs wydaje sie byc wygodniejszy dla listenerow typu Logger

    Listener z Listing nr 2 narzuca zaimplementowanie sciezki w przypadku niepowodzenia wykonania. Wygodniejszy i czystszy dla listener-a biznesowego.

  2. W pierwszym listenerze najważniejszą kwestią jest powiadomienie o tym, że usługa zwróciła coś – sam wynik jest nieistotny. W drugim mamy rozgraniczenie na wynik na poziomie metod, co jest dobrym rozwiązaniem gdyż mamy zachowaną regułę, że jedna metoda robi jedną funkcjonalność (zamiast w instrukcji warunkowej każdorazowo sprawdzać ten wynik). Więc drugiego listenera trzeba by używać kiedy są różne ścieżki w zależności od wyniku (lub jest prawdopodobieństwo, że te ścieżki kiedyś będą się różnić)

  3. Ja wybrałbym pierwszy, jest bardziej uniwersalny. Po za tym w całym SWINGU właśnie jest stosowany taki onEvent. Jakoś wolę jak jeden listener obsłuży mi start, koniec, sukces i porażkę operacji.

  4. A ja bym powiedział, że nie tędy droga. To są kompletnie różne sytuacje i ich obsługa powinna być zupełnie gdzie indziej (SRP?).

    Albo interesują mnie sprawy domenowe, albo stan usługi. Albo coś się zmienia w danych (wtedy chcę odświeżyć GUI albo wykonać jakąś akcję biznesową, cokolwiek to znaczy), albo mam problem z usługą (wtedy mogę chcieć wysłać maila, wyświetlić komunikat o błędzie albo zapisać inny rodzaj loga).

    Dlaczego jakaś tabelka w GUI miałaby się martwić błędami usługi, albo dlaczego logger błędów miałby wiedzieć o zmianach domenowych?

    Innymi słowy, może by tak dwa interfejsy? Zależnie od frejmworkowatości, CUDObserver lub MyEntityGuiObserver (dobre nazwy, nie?) plus ServiceCallStatusObserver.

  5. Tak trochę po ptakach ale dorzucę rzecz, której omówienia nie zauważyłem: nie zawsze mamy po prostu sukces i błąd. Albo, inaczej mówiąc, miewamy różne rodzaje błędów.

    Wywołanie usługi „wykonaj przelew” może się skończyć powodzeniem, może błędem „zbyt długi tytuł przelewu”, może błędem „brak środków na rachunku”, może błedem „zablokowany rachunek”, może błędem „niedostępność systemu bankowego”, może też błędem „database failure, can’t extend tablespace” (i różnymi innymi po drodze). Zgoła różne warstwy kodu mogą być odpowiedzialne i za klasyfikowanie takich błędów i za ich obsługę.

    Na dzisiaj nie znam lepszego rozwiązania niż odpowiednio rozsądne hierarchie wyjątków (przy czym w samym procesie ich obsługi jakieś modele subskrypcji zdarzeń bywają przydatne). Ale jeśli ograniczać się do interfejsów w stylu powyższego drugiego (pierwszy oczywiście mieści wszystko ale wymusza rozdmuchiwanie kodu po stronie zainteresowanych) to przynajmniej rozdzieliłbym onError na onTechnicalError i onBusinessError czy coś podobnego.

    Disclaimer: nie piszę w Javie

Napisz odpowiedź

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *

To create code blocks or other preformatted text, indent by four spaces:

    This will be displayed in a monospaced font. The first four 
    spaces will be stripped off, but all other whitespace
    will be preserved.
    
    Markdown is turned off in code blocks:
     [This is not a link](http://example.com)

To create not a block, but an inline code span, use backticks:

Here is some inline `code`.

For more help see http://daringfireball.net/projects/markdown/syntax