Dawno dawno temu pisałem dlaczego należy opakowywać kolekcje w obiekty domenowe i posługiwać się ich abstrakcją, a nie gołymi kolekcjami. Niedawno pobawiliśmy się też niepustymi kolekcjami. Wczoraj znowuż na 4P pojawiło się pytanie jak zrobić „statek”.

Motywacja

Nasz statek jest to value object opakowujący kolekcję, który ma narzuconą pewną specyficzną walidację na poziomie dodawania obiektów. Spróbujmy połączyć te kawałki wiedzy w coś sensownego. Przypomnijmy sobie przykład z niepustą kolekcją:

Listing 1. Użycie kolekcji nie pustej

public class ValidationPresenter {

	public String showMessages(NotEmptyCollection<ConstraintViolation> errors) {
		Predicate<ConstraintViolation> nn = cv -> cv != null;
		Predicate<ConstraintViolation> ne = cv -> !cv.getMessage().trim().isEmpty();
		String errorMessage = errors.stream()
				.filter(nn.and(ne))
				.map(cv -> cv.getMessage())
				.reduce("", (l, r) -> String.format("%s\n%s", l, r));
		Preconditions.checkArgument(!errorMessage.trim().isEmpty(), "Error message cannot be empty");
		return errorMessage;
	}
}

Z naszej nie nullowej (założenie i tego predykatu nie ma w kodzie) kolekcji wyciągamy kolejno elementy i odrzucamy te, które są null albo mają pustą wiadomość. Następnie składamy komunikat i wysyłamy go w świat. Dodatkowe sprawdzenie na końcu pozwala na wychwycenie sytuacji gdy wszystkie elementy w kolekcji odpadną na filtrowaniu.

Spróbujmy z tego kodu usunąć filtry w taki sposób by mieć pewność iż nasza kolekcja wejściowa zawsze zawiera poprawne elementy. To pozwoli nam na usunięcie też sprawdzenia na wyjściu ponieważ wiemy, że żaden element nie został odrzucony i jest co najmniej jeden z niepustą wiadomością zatem efektem będzie nie pusta wiadomość.

Co trzeba zmienić

Zmiany w klasie NotEmptyList są stosunkowo proste. W metodach dodających elementy do kolekcji oraz na poziomie konstruktora musimy sprawdzać czy dodawany element spełnia warunki. Same warunki będą sprawdzane z pomocą interfejsu Verifier

Listing 2. Interfejs Verifier

public interface Verifier<T> {

	T verify(T t);
}

Potrzebna nam jest też klasa NotEmptyAbstractListWithVerifier, która będzie wyglądać w następujący sposób:

Listing 3. Klasa NotEmptyAbstractListWithVerifier

public abstract class NotEmptyAbstractListWithVerifier<T, D extends Verifier<? super T>> extends
		NotEmptyAbstractList<T>
		implements NotEmptyListWithVerifier<T, D> {

	private final D verifier;

	public NotEmptyAbstractListWithVerifier(D verifier, T element) {
		super((T) verifier.verify(element));
		this.verifier = verifier;
	}

	protected D getVerifier() {
		return verifier;
	}

	@Override
	public T set(int index, T element) {
		verifier.verify(element);
		return super.set(index, element);
	}

	@Override
	public void add(int index, T element) {
		verifier.verify(element);
		super.add(index, element);
	}

	@Override
	public boolean add(T t) {
		verifier.verify(t);
		return super.add(t);
	}

	@Override
	public boolean addAll(Collection<? extends T> c) {
		return c.stream().map(e -> this.add(e)).reduce(false, (l, r) -> l || r);

	}

	@Override
	public boolean addAll(int index, Collection<? extends T> c) {
		c.forEach(verifier::verify);
		return super.addAll(index, c);
	}
}

Weryfikator powinien sypnąć jakimś specyficznym błędem. Jakim to nie ważne, bo będzie zależało od konkretnej implementacji.

Zmiany w showMessages

Na koniec zestaw zmian w sygnaturze metody showMessages. Chcemy by jako parametr przyjęła ona nie pustą kolekcję, która ma specyficzny weryfikator na pokładzie.

Listing 4. Klasa ValidationPresenter z weryfikacją

public class ValidationPresenter {

	public <V extends NotNullVerifier<ConstraintViolation> & NotEmptyVerifier<ConstraintViolation>>
	String showMessages(NotEmptyCollectionWithVerifier<ConstraintViolation, V> errors) {
		String errorMessage = errors.stream()
				.map(cv -> cv.getMessage())
				.reduce("", (l, r) -> String.format("%s\n%s", l, r));
		return errorMessage;
	}
}

Zapis jest paskudny, ale java już taka jest. Jako argumentu oczekujemy nie pustej kolekcji z weryfikatorem typu V, który jest jednocześnie NotNullVerifier i NotEmptyVerifier. Co to oznacza? Oznacza to iż na wejściu mamy pewność, że nasza kolekcja zawiera tylko poprawne elementy. Zatem nie ma potrzeby aplikowania dodatkowych filtrów czy walidacji wyjścia.

Weryfikatory

Ostatnią rzeczą, która nam została to przyjrzenie się weryfikatorom. Kod z listingu 4 podpowiada, że powinny to być interfejsy generyczne. Przynajmniej na poziomie pewnej abstrakcji.

Listing 5. Interfejs NotNullVerifier z weryfikacją

public interface NotNullVerifier<T> extends Verifier<T> {

	@Override
	default T verify(@NotNull  T t) {
		return Preconditions.checkNotNull(t);
	}
}

Tu sprawa jest prosta. Każdy obiekt w Javie jest nullem w ten sam sposób. Możemy zatem zaszyć w nim regułę.

Listing 6. Interfejs NotEmptyVerifier z weryfikacją

public interface NotEmptyVerifier<T> extends Verifier<T> {

}

Co oznacza, że obiekt jest nie pusty? To zależy od konkretnej klasy. Zatem tu pozostawiamy wszystko bez zmian. Interfejs będzie pełnił tylko rolę znacznika.

I teraz to musimy spiąć w postaci klasy CVMixedVerifier.

Listing 7. Klasa CVMixedVerifier z weryfikacją

public class MixedCVVerifier implements NotEmptyVerifier<ConstraintViolation>, NotNullVerifier<ConstraintViolation> {
	@Override
	public ConstraintViolation verify(@NotNull  ConstraintViolation o) {
		NotNullVerifier.super.verify(o);
		Preconditions.checkArgument(!o.getMessage().trim().isEmpty());
		return o;
	}
}

Najpierw sprawdzamy warunek z NotNullVerifier, a następnie dodajemy implementację z NotEmptyVerifier. W ten sposób można już wykonać nasz kod w bezpieczny sposób.

Podsumowanie

Stworzyliśmy kod, który jest bezpieczny z punktu widzenia biznesu. Gwarantując sobie za pomocą typów pewne właściwości elementów kolekcji możemy tworzyć kod, który będzie w dużej mierze „idiotoodporny”. Nie da się wywołać metody showMessages przekazując nieprawidłową, pustą, kolekcję ani też przekazując kolekcję zawierającą nieprawidłowe elementy. Oczywiście weryfikacja elementów nadal odbywa się w czasie działania programu i tu dotykamy trochę innego tematu.

Istnieje możliwość wykorzystania JSR-305, który ma za zadanie detekcję bugów w kodzie. Jednak status tej specyfikacji to Dormant co oznacza „zdechłem i nie nie ożywią”. Można też korzystać ze zmian wniesionych przez JSR-308, który definiuje nowe miejsca dla adnotacji. Lecz nadal wszystkie te elementy żyją na poziomie uruchomienia, a nie kompilacji.