Funkcje w Guavie i dekoratory
Najprostszy do zaimplementowania wzorzec obiektowy, poza singletonem, w programowaniu z użyciem Function i Guavy, czyli dekorator. Dlaczego najprostszy? Ponieważ jest to podstawa działania funkcji wyższego rzędu (ang. High Order Function), które znowuż są podstawą tworzenia trochę bardziej zaawansowanych zabawek.
Motywacja
klasycznym przykładem dekoratora jest budowanie GUI gdzie kolejne elementy staramy się „udekorować” pewnymi dodatkami. W moim przypadku dekorator rozwiązał trochę inny problem, ale podobnej klasy. Okazało się, że potrzebujemy logować wejście, wyjście i czas wykonania funkcji. Banał. Można napisać kod loggera bezpośrednio w funkcji i olać, co IMO, nie jest rozsądne. Z kilku powodów, ale dwa najważniejsze to wiązanie kodu funkcji z logowaniem, co jest wtłaczaniem pewnej dodatkowej odpowiedzialności oraz silne wiązanie pomiędzy funkcją, a logiem.
Powiedzmy sobie szczerze, im prostsza funkcja tym lepiej. Proste funkcje w Guavie można składać za pomocą predykatów i kompozycji w bardziej rozbudowane przepływy. Jeżeli wszyjemy na sztywno jakieś dodatkowe elementy do funkcji – grób mogiła. Nie ma szans na wygrzebanie się z tego gówna. To wynika z pierwszego powodu. Jedna funkcja – jedna odpowiedzialność. Drugi powód ma źródło w pierwszym Jeżeli naszą funkcję w jakiś sposób powiążemy z logowaniem to nie pozbędziemy się tego wrzodu na dupie bez grzebania w kodzie samej funkcji.
Sytuacja idealna – tworzymy sobie projekcik w którym są same funkcje podstawowe, plus kilka funkcji wyższego rzędu, które będą intensywnie używane bądź reprezentują pewne wiadome elementy systemu. Klient dowolnie składa te nasze funkcje tworząc odpowiednie przepływy biznesowe. O tym jak prosta powinna być funkcja innym razem.
Jeżeli zatem klient chce mieć w jednym przepływie logowanie danej operacji, a w innym nie to nasz kod powinien być tak przygotowany, by dodanie logowania było deklaratywne.
Prosta implementacja, czyli tu nie ma rocket science
Oczywiście nasza implementacja powinna opierać się na funkcjach tak by nie zaburzać konstruowania przepływów. Ponad to nie powinna ona zmieniać interfejsu funkcji dekorowanej. Inaczej mówiąc potrzebujemy czegoś co przyjmie nam naszą oryginalną funkcję jako argument zrobi swoje „czary mary” i przy okazji odpali tą oryginalną funkcję bez modyfikowania wejścia, po czym zwróci jej niezmodyfikowany wynik.
Listing 1. To nie jest rocket science
public class DecoratorLogger<I, O> implements Function<I, O> {
private final Function<I, O> originalFunction;
private final Logger logger;
private Level level;
public static <I, O> Function<I, O> info(Function<I, O> originalFunction){
return new DecoratorLogger<>(originalFunction, Level.INFO);
}
public static <I, O> Function<I, O> warning(Function<I, O> originalFunction){
return new DecoratorLogger<>(originalFunction, Level.WARNING);
}
public static <I, O> Function<I, O> serve(Function<I, O> originalFunction){
return new DecoratorLogger<>(originalFunction, Level.SEVERE);
}
private DecoratorLogger(Function<I, O> originalFunction, Level level) {
this.originalFunction = originalFunction;
this.level = level;
this.logger = Logger.getLogger(originalFunction.getClass().getCanonicalName());
}
@Override
public O apply(I input) {
Stopwatch watch = Stopwatch.createStarted();
O output = originalFunction.apply(input);
Stopwatch stop = watch.stop();
logger.log(level, "", new Object[]{input, output, "" + stop.elapsed(TimeUnit.MILLISECONDS)});
return output;
}
}
Implementacja naiwna jest naiwna i ja o tym wiem. W tym konkretnym przypadku jest ona silnie powiązana z konkretnym zadaniem. W dodatku używam loggera z JDK, co oznacza, że mam dostępne poziomy. W przypadku użycia slf4j nie mam metody w rodzaju log przyjmującej poziom logowania. Nie jest to też zbyt uniwersalne rozwiązanie. Spróbujmy zatem czegoś innego.
Abstrakcyjny dekorator
Co robi nasz dekorator w tym przypadku? Robi COŚ przed wywołaniem funkcji, wywołuje funkcję, po czym robi COŚ po jej wywołaniu i następnie zwraca wynik. Wygląda to jak metoda szablonowa…
Listing 2. Dekorator abstrakcyjny
public abstract class AbstractDecorator<I, O> implements Function<I, O> {
protected final Function<I, O> originalFunction;
protected AbstractDecorator(Function<I, O> originalFunction) {
checkNotNull(originalFunction);
this.originalFunction = originalFunction;
}
protected abstract void before(I input);
protected abstract void after(I input, O output);
@Override
public O apply(I input) {
before(input);
O output = originalFunction.apply(input);
after(input, output);
return output;
}
}
Pozostało w sumie zrobić tylko kilka małych prac w naszym oryginalnym kodzie, ale to ćwiczenie dla ciebie. Pierwsza refaktoryzacja będzie oczywiście banalnie prosta, ale jak już ją zrobisz to zastanów się jak zachowa się kod w środowisku wielowątkowym.
Podsumowanie
Programowanie funkcyjne z użyciem Guavy choć jest bardzo mocno ograniczone w porównaniu do na przykład Scali to jednak daje nam możliwość wprowadzenie konstrukcji, których użycie będzie łatwe, proste i w miarę przyjemne. Możliwość użycia dekoratorów, które są najprostszymi funkcjami wyższego rzędu daje znowuż odpowiednie rozwiązania pozwalające na szybkie obudowywanie istniejącego kodu w pewne dodatkowe rozwiązania bez konieczności ingerowania w główny element. To już jest bardzo dobre 🙂
Jeżeli spodobał ci się wpis podziel się nim ze znajomymi korzystając z przycisków poniżej. Możesz też wysłać link do niego mailem lub przez komunikator.