Throw to taki inny return…

Przynajmniej w Javie. Jak już mamy Javę 8 to możemy pokusić się o takie podejście pod pewnymi warunkami.

Przypadek

Klient ma Sonara i nie zawahał się go użyć. Rzecz w tym, że ocenie podlega tylko kod z zewnątrz organizacji, czyli nasz, a ten wewnętrzny może być chujowy. My przejęliśmy ten chujowy kod i teraz naprawiamy…
Jednym z elementów jest maszynka do zamiany jsonów na obiekty i na odwrót. W niej jest kawałek kodu do pracy z datami. Wygląda on mniej więcej tak

Listing 1. Konwerter dat

public class DateConverting {

	public static void main(String[] args) {
		String dateJson = "1"; // 1-1-1970 :D

		Date date = convertStringToDate(dateJson);
		System.out.println(date.toString());
	}

	private static Date convertStringToDate(String s) {
		if (s == null || s.trim().isEmpty()) return null;
		try {
			return new SimpleDateFormat("yyyy-MM-dd'T'hh:mm:ss.SSS").parse(s);
		} catch (ParseException e) {
		}
		try {
			return new SimpleDateFormat("yyyy-MM-dd'T'hh:mm:ss").parse(s);
		} catch (ParseException e) {
		}
		try {
			return new SimpleDateFormat("yyyy-MM-dd'T'hh:mm").parse(s);
		} catch (ParseException e) {
		}
		try {
			return new SimpleDateFormat("yyyy-MM-dd").parse(s);
		} catch (ParseException e) {
		}
		return new Date(Long.valueOf(s));
	}
}

Mamy cztery formaty daty i jeżeli String nie pasuje do żadnego z nich to traktujemy jak zwykły long. Jeżeli jest pusty albo jest nullem to zwracamy null. Straszna paskuda. Sonar też to stwierdził i strasznie czerwony raport. Klient kręci germańską główką i szwargocze… Jak to można naprawić.

Wersja prosta

Na razie rzecz prosta i bez udziwnień. Generalnie mamy sobie dopuszczone formaty, robimy z nich listę iterujemy i jak na listingu 2

Listing 2. Konwerter dat pierwsza poprawka

private static Date convertStringToDate(String s) {                           
	if (s == null || s.trim().isEmpty()) return null;                         
	ArrayList<String> patterns = Lists.newArrayList(YYYY_MM_DD_T_HH_MM_SS_SSS,
			YYYY_MM_DD_T_HH_MM_SS                                             
			, YYYY_MM_DD_T_HH_MM                                              
			, YYYY_MM_DD);                                                    
	for (String pattern : patterns) {                                         
		try {                                                                 
			return new SimpleDateFormat(pattern).parse(s);                    
		} catch (ParseException e) {                                          
		}                                                                     
	}                                                                         
	return new Date(Long.valueOf(s));                                         
}

Generalnie to rozwiązanie jest takie samo jak poprzednie. Różnica jest na poziomie „zamiast stu zmiennych zrób tablicę”.
My oczekujemy czegoś innego. Teraz będzie dużo kodu i magii javy 8 oraz trochę programowania prawie funkcyjnego 😉 Da się to rozwiązanie zaimplementować też z użyciem np. Guavy, ale to jak musicie mieć javę 7.

Użyjmy javy 8

Koncepcja tej refaktoryzacji opera się o stwierdzenie, że rzucenie wyjątku jest tak na prawdę rodzajem instrukcji powrotu. Nasza metoda ma w takim przypadku dwa możliwe typu na wyjściu. Jeden ten zadeklarowany i drugi dla wyjątku.

Interfejs opakowujący

Na początek zdefiniujmy sobie interfejs opakowujący ParseTask. Pozwoli on na ujednolicenie różnych mechanizmów parsowania.

Listing 3. Interfejs ParseTask

interface ParseTask<R>{
	R parse() throws ParseException;
}

Implementacja będzie realizować zadanie parsowania. Może zwrócić coś R albo wyrzucić wyjątek ParseException.

Rezultat parsowania

Potrzebujemy też klasę, która będzie reprezentować rezultat parsowania. Nie chcę używać Optional ponieważ jest zbyt ogólny, a jeżeli chcemy gdzieś zachować informacje o błędzie to musimy mieć klasę specjalizowaną.

Listing 4. Klasa ParseResult

abstract class ParseResult<R> {

	static class Valid<R> extends ParseResult<R>{
		final R r;

		Valid(R r) {
			this.r = r;
		}

		@Override
		public boolean isValid() {
			return true;
		}

		@Override
		public R result() {
			return r;
		}
	}

	static class Failed<R> extends ParseResult<R>{
		final ParseException e;

		Failed(ParseException e) {
			this.e = e;
		}

		@Override
		public boolean isValid() {
			return false;
		}

		@Override
		public R result() {
			throw new NoSuchElementException("No value present");
		}
	}

	public abstract boolean isValid();
	public abstract R result();
}
Maszynka do uruchamiania

Czyli klasa Parser, której zadaniem jest uruchamianie poszczególnych zadań parsowania i tworzenie odpowiednich wyników

Listing 5. Klasa Parsersamp>

class Parser<R> implements Function<ParseTask<R>, ParseResult<R>> {

	@Override
	public ParseResult<R> apply(ParseTask<R> parseTask) {
		try {
			return new ParseResult.Valid(parseTask.parse());
		} catch (ParseException e) {
			return new ParseResult.Failed(e);
		}
	}
}

Uruchamiamy

Finalnie nasza metoda konwertująca może wyglądać w następujący sposób:

Listing 5. Klasa Parser

private static Date convertStringToDate(String s) {
		if (s == null || s.trim().isEmpty()) return null;
		ArrayList<String> patterns = Lists.newArrayList(YYYY_MM_DD_T_HH_MM_SS_SSS,
				YYYY_MM_DD_T_HH_MM_SS
				, YYYY_MM_DD_T_HH_MM
				, YYYY_MM_DD);

		return patterns.stream()
				.map(p -> (ParseTask<Date>) () -> new SimpleDateFormat(p).parse(s))
				.map(new Parser<>())
				.filter(ParseResult::isValid)
				.map(ParseResult::result)
				.findFirst().orElseGet(() -> new Date(Long.valueOf(s)));

	}

Najpierw tworzymy sobie listę zdań parsowania. Następnie odpalamy parser (feralna nazwa), sprawdzamy czy wynik jest poprawny i pierwszy poprawny wynik zwracamy. Jeżeli nie będzie żadnego poprawnego wyniku to dokonujemy domyślnej konwersji.

Podsumowanie

Czy kod wynikowy jest lepszy? Jeżeli ta operacja konwersji i parsowania była by tylko w jednym miejscu w całym systemie to można takie rozwiązanie potraktować jako ciekawostkę. Jeżeli mamy jednak wiele różnych praserów i konwersji to można wprowadzić prostą klasę narzędziową, która ułatwi nam życie:

Listing 6. Klasa ParserMatcher

class ParserMatcher<R>{

	public R findParserAndParse(Collection<ParseTask<R>> pt, Supplier<R> defVal){
		return pt.stream()
				.map(new Parser<>())
				.filter(ParseResult::isValid)
				.map(ParseResult::result)
				.findFirst().orElseGet(defVal);
	}
}

i pozwoli zredukować kod do kilku prostych regułek.

Cały „myk” w tym rozwiązaniu opiera się na dwóch rzeczach. Pierwsza to redukcja efektu ubocznego jakim jest wyjątek do poziomu opcjonalnego wyjścia. Efekt nie opuszcza miejsca, w którym się pojawił. Zostaje opakowany i zwrócony jako rezultat. Druga to rozgraniczenie sposobu przetwarzania, czyli parsowania od danych, czyli w tym przypadku parserów. W pewnym sensie tworzymy tu bardzo prymitywną gramatykę i efektywnie język obróbki informacji.

6 myśli na temat “Throw to taki inny return…

  1. A może użyj DateTimeFormattera skoro już Jave 8 masz. Jest threadsafe 🙂

  2. @Michał wiem, że jest. Tu chodzi o pokazanie pewnej koncepcji, a na datach jest ona prosta do zrozumienia.

  3. Dobre rozwiązanie. Sonar nie wie co się dzieje i pluje na zielono. Nie znaczy to, że kod jest łatwiejszy w zrozumieniu. Ile czasu należy poświęcić na zrozumienie wersji czerwonej a ile na zielonej? Lepiej chyba byłoby spacyfikować klienta.

  4. Klienta nie spacyfikujesz, bo on stosuje tego sonara do jakiś 500 projektów.

    Zresztą zauważyłem ciekawą rzecz. Wielu programistów mówi o braku czytelności kodu opartego o świętą trójcę (map, filter, foreach), ale jak ich poprosić o przeczytanie na głos co kod robi to okazuje się, że idzie to naturalniej.

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