Mamy sobie taki sprytny inaczej system uruchamiania raportów w innym systemie, gdzie z jednej strony mamy status raportu jako enum, a z drugiej jako String. Klasyczny przypadek prowadzący do „potworkowania” kodu takimi oto ifami:

Listing 1. If-potworek

for (ReportRunHistory reportRunHistory : reportRunsHistory) {                     
	LOG.info(reportRunHistory.toString());                                        
	if (reportRunHistory.status.equals(CANCELED.getStatusText())                                  
			|| reportRunHistory.status.equals(FAILED.getStatusText())                             
			|| reportRunHistory.status.equals(COMPLETED.getStatusText())){
        // ...
        }                                       
}

Tomek Nurkiewicz na Confiturze w 2012 roku opowiadał o tym jak pozbywać się Ifów z kodu. Ten kod podpada pod „jest prosto nie trzeba upraszczać”. Mnie jednak osobiści drażni takie coś. Można to naprawdę sprowadzić do czegoś co będzie wyglądać dobrze i co najważniejsze będzie łatwe do przeczytania. Jak używamy Guavy rzecz staje się jeszcze prostsza.

Do czasu…

Pierwszy problem każdego korpo

Trochę się po korpo nie informatycznych włóczyłem i zaobserwowałem, że istnieje w nich pewna wspólna cecha dotycząca kodu. Można by ją opisać, jako mieszankę niechęci do zmian z silną separacją zespołów. Wyobraźmy sobie, że nasz system raportowy ma klienta, który jest napisany w Javie i dostarczony nam przez zespół zajmujący się apką raportową. Rzecz w tym, że jak znajdziemy jakiś błąd to procedura wdrożenia poprawki jest drogą przez mękę. W praktyce napisanie własnego lokalnego haka na bibliotekę jest prostsze…
Inaczej mówiąc w korpo jak dostajesz coś zapaczkowane w jara, to choć by zespół odpowiedzialny za ten kod siedział po sąsiedzku to uzyskanie poprawki jest długotrwałe.
A co w przypadku gdy chcesz uzyskać nową funkcjonalność? Zapomnij.

Miej więcej taki problem mamy z naszym systemem raportowym. Choć klient mógłby zwracać enum to zwraca String, a my musimy robić brzydkie haki w naszych enumach w rodzaju:

Listing 2. „Hakowany” enum

public enum RunReportStatusEnum {

	QUEUED("queued"), RUNNING("running"), COMPLETED("completed"), FAILED("failed"), CANCELED("canceled");

	private final String statusText;

	private RunReportStatusEnum(String statusText) {
		this.statusText = statusText;
	}

	public String getStatusText() {
		return statusText;
	}

}

String w konstruktorze enuma, który mapuje nasz kod na zasadzie enum-String. Można to rozwiązać mapą, ale… nadal zostaje ten pieprzony If z Listingu 1 gdzie jakoś musimy w pewnym momencie przefiltrować sobie to co przychodzi na nasze enumy.

No ale mamy Guavę

A Guava ma predykaty. Pisałem już o predykatach i funkcjach, a teraz chcę pokazać jedno z ciekawszych zastosowań, moim zdaniem oczywiście.

Krok 1. Enum – predykat

Pierwszym krokiem refaktoryzacji jest zauważenie, że w IFie kod składa się z trzech identycznych pod względem konstrukcji warunków. Można je zgeneralizować w następujący sposób:

Listing 3. Metoda uogólniająca dla Ifa

boolean is(ReportRunHistory reportRunHistory, RunReportStatusEnum e){
	return reportRunHistory.status.equals(e.getStatusText());
}

Co w samo w sobie jest może i ładne, ale użycie tego w kodzie nadal paskudne. Zatem w zamiast czegoś takiego można napisać własny enum implementujący Predicate:

Listing 4. Enum – predykat

public enum RunReportStatusEnumPredicate implements Predicate<String> {
	IS_QUEUED(QUEUED), 
	IS_RUNNING(RUNNING), 
	IS_COMPLETED(COMPLETED), 
	IS_FAILED(FAILED), 
	IS_CANCELED(CANCELED);

	private final RunReportStatusEnum statusEnum;

	private RunReportStatusEnumPredicate(RunReportStatusEnum statusEnum) {
		this.statusEnum = statusEnum;
	}

	@Override
	public boolean apply(String input) {
		return statusEnum.getStatusText().equals(input);
	}
}

W praktyce nie wychodzi to poza to co na listingu 3. Główna różnica polega na tym, że całość można sprawnie przetestować (banalne) oraz mamy zapewniony interfejs zdatny do użycia z Guavą.

Krok 2. Składamy predykat

Kto czytał tekst o predykatach ten wie, że można składać predykaty wykorzystując metodę Predicates.or. Złóżmy zatem nasz predykat:

Listing 5. Enum – predykat

or(IS_CANCELED, IS_FAILED, IS_COMPLETED)

LOL… jakie to proste… nasz wyjebany w kosmos IF zamienił się w jednolinijkowca… Nope…

Krok 3. Transformata

Jak przyjrzycie się interfejsowi naszego predykatu to przyjmuje on typ String, a w pętli mamy do czynienia z ReportRunHistory. Jak zatem wtłoczyć do naszego kodu inny typ? Tu z pomocą przychodzi kolejna metoda Predicates.compose, która przyjmuje predykat i funkcję, która zwraca typ zgodny z typem wejściowym predykatu.

Listing 6. Funkcja konwertująca

public class ReportRunHistoryStatusAsStringFunction implements Function<ReportRunHistory, String> {
	public static final ReportRunHistoryStatusAsStringFunction REPORT_RUN_HISTORY_STATUS_AS_STRING_FUNCTION
			= new ReportRunHistoryStatusAsStringFunction();
	
	private ReportRunHistoryStatusAsStringFunction(){}

	@Override
	public String apply(ReportRunHistory input) {
		return input.status;
	}
}

W efekcie nasz kod można przepisać do:

Listing 7. Funkcja i enum

compose(
		or(IS_CANCELED, IS_FAILED, IS_COMPLETED),
		REPORT_RUN_HISTORY_STATUS_AS_STRING_FUNCTION
)

Co zwraca predykat przyjmujący ReportRunHistory i odpalający warunek z predykatu gdzie potrzeba String.

Pułapka

Kod zaczął się trochę „rozwlekać”. Można mu zarzucić, że stworzyłem wiele małych klas, których powtórne użycie jest wątpliwe, a trzeba je jakoś utrzymywać. Powiem tak, jeżeli boisz się tworzyć nowe klasy „na masę” to idź pisać w pascalu gdzieś indziej. Kluczem do sukcesu w tym przypadku jest duża granulacja kodu. W praktyce ( i w rozwiązaniu z korpo) z pojedynczej konstrukcji for-if-for-if-logika można wygenerować nawet 10-12 klas, a każda z nich będzie odpowiadać, za jakąś pojedynczą funkcjonalność. Choćby była to obsługa ifa.
Ten przykład jest niekompletny, ponieważ if znajduje się w pętli to nie do końca widać siłę tej refaktoryzacji. Ale o tym następnym razem, bo nie o tym tu chciałem teraz napisać.

Duża granulacja kodu to też pewna pułapka. Z jednej strony bardzo łatwo jest taki kod przetestować, ale z drugiej strony kto będzie pisał testy dla getterów (nasza funkcja to opakowanie gettera). tym samym może okazać się, że po większej refaktoryacji, gdzie dodaliśmy dużo nowych małych klas bez testów, nagle nasze pokrycie testami spada i wywala buildy. Bywa… choć testowanie małych klas wydaje się zawracaniem dupy to należy się zmusić i to robić.
Można też ułatwić sobie życie testując trochę większe kawałki kodu. Tu dobrym kandydatem na napisanie testu jest nie funkcja i enum, ale predykat powstały z ich kompozycji. Choć nadal duża część testu to sprawdzenie „czy guava działa” to jednak jest to już test, który już napisaliśmy. Kiedy? Gdy przystępowaliśmy do refaktoryzacji i mieliśmy pokrycie testami dla całego Ifa. Pamiętajcie, że nadal testujemy jakiś większy kawałek kodu – serwis czy coś w ten deseń, gdzie w środku w wyniku refaktoryzacji udało się wyciągnąć ileś tam małych klas.

Podsumowanie

Przedstawiona tu technika radzenia sobie z ifem jest dobrym punktem zaczepienia jeżeli chcecie wprowadzać elementy programowania funkcyjnego do swojego kodu. Nie jest to oczywiście programowanie funkcyjne jako takie, ale ma jego naturę (funkcja i predykat są bezstanowe, nie zmieniają stanu obiektu itp.). To już dużo. Znacznie więcej niż można sobie wyobrazić gdy widzimy przeciętny kod korpo-potworka i chcemy się w nim rozeznać.

Jest to oczywiście fragment większej refaktoryzacji, a kolejnym ciekawym jej elementem jest to jak wywołać logger z pierwszej linii pętli.

O tym między innymi będę mówił w trakcie warsztatów „Beginning Functional Programming in Java world” na Warsjawie już 26-27 września.

Na koniec mała prośba jeżeli spodobał ci się ten wpis udostępnij go w mediach społecznościowych korzystając z przycisków poniżej. Jak masz pytania zadaj je w komentarzu. Postaram się zaspokoić twoją ciekawość.