JSR-303, a spadki

Specyfikacje JSR mają w większości „problem COBOLa” polegający mniej więcej na tym, że dobry pomysł chce się zamknąć w naukowo-techniczny dokument. Tak też jest w przypadku JSR-303. Tu jednak zabawa polega na delegowaniu pewnych zachowań do specyfikacji javy, co może skończyć się całkiem widowiskowym failem. W czym rzecz.

Mamy sobie pewien model danych, a w nim klasę SomeClass:

Listing 1. Klasa SomeClass

public class SomeClass {

	@NotNull
	private final Integer someInt;

	public SomeClass(Integer someInt) {
		this.someInt = someInt;
	}

	public Integer getSomeInt() {
		return someInt;
	}

}

Ma ona jedno pole, które niesie ze sobą jakąś tam informację biznesową. Bez wnikania w szczegóły pole nie może być null. Możemy zatem założyć, że taki test:

Listing 2. Test SomeClass

public class SomeClassTest {

	private Validator validator;

	@BeforeClass
	protected void setUp() {
		validator = Validation.buildDefaultValidatorFactory().getValidator();
	}
	
	@Test
	public void simpleValidationForClass() {
		assertThat(validator.validate(new SomeClass(1))).isEmpty();
		assertThat(validator.validate(new SomeClass(null))).isNotEmpty();
	}

}

Jest ok.

W toku prac nad kodem dochodzimy do miejsca, w którym musimy wprowadzić nową podklasę klasy SomeClass, która zachowuje się tak samo, ale jej pole może przyjmować tylko wartość null. Musimy zatem przesłonić pole albo metodę w klasie SubSomeClass. Ja wybrałem bardziej intuicyjny element – pole.

Listing 3. Klasa SubSomeClass

public class SubSomeClass extends SomeClass {

	@Null
	private final Integer someInt;

	public SubSomeClass(Integer someInt) {
		super(someInt);
		this.someInt = someInt;
	}
	
	@Override
	public Integer getSomeInt() {
		return someInt;
	}
}

Test, który powinien przejść w takim wypadku wygląda tak:

Listing 4. Test SubSomeClass

public class SubSomeClassTest {
	private Validator validator;

	@BeforeClass
	protected void setUp() {
		validator = Validation.buildDefaultValidatorFactory().getValidator();
	}

	@Test
	public void simpleValidationForSubClass() {
		assertThat(validator.validate(new SubSomeClass(null))).isEmpty();
		assertThat(validator.validate(new SubSomeClass(1))).isNotEmpty();
	}
}

… jednak z pewnego powodu test nie przechodzi.

JSR-303 u notariusza

Dziedziczenie jak wiadomo jest najlepszą, obiektową metodą na wzbogacenie się. Dziedzicząc jednak z klasy robimy to w prost co oznacza, że wzbogacamy się o wszystkie elementy klasy z której dziedziczymy jak i odpowiadamy za wszelkie długi (technologiczne), które dana klasa zaciągnęła. Tak to wygląda w przypadku klas.
JSR-303 ma tu jednak pewne pole do manewru ponieważ posługując się refleksją może spróbować innych sposobów zarządzania technologicznym „spadkiem”.

  • Może odrzucić spadek w całości o nie ma sensu ponieważ nie będzie wtedy weryfikował nieprzesłoniętych pól nadklasy.
  • Może przyjąć spadek „z dobrodziejstwem inwentarza”, czyli w przypadku gdy w nadklasie jest już pole o takiej samej nazwie to nie uruchamia jego walidacji.

Niestety twórcy specyfikacji poszli po linii najmniejszego oporu delegując zachowanie do „zasad widoczności w specyfikacji języka”. Efekt możecie podziwiać wyżej.

Jak sobie z tym poradzić?

Najprościej jest wnieść walidację na poziom całej klasy poprzez własny walidator, ale na dłuższą metę to też nie zadziała. W Walidatorze i tak będziemy musieli sprawdzać typ obiektu. Drugą metodą jest rozbudowa hierarchii w taki sposób by klasa SomeClass stała się klasą abstrakcyjną, a pola umieścić w klasach z niej dziedziczących. Już lepiej choć nadal niedobrze, bo mamy dublujący się kod. Jest jeszcze mechanizm grup, ale nie do tego on służy (wbrew pozorom), bo i tak nie unikniemy tu sprawdzania typu.

Jeff Bay przychodzi z pomocą

W komentarzach do trzeciej części EOwP pojawiły się głosy, że dodatkowa warstwa nie jest potrzebna, bo tyko komplikuje kod. Tu jednak mamy dobre miejsce na wprowadzenie typu obudowującego.
Na początek definiujemy interfejs BussinesInt w klasie SomeClass:

Listing 5. Interfejs BussinesInt

public class SomeClass {

	protected static interface BussinesInt {
		Integer getValue();
	}

//...
}

Następnie dodajemy implementację w ramach klasy SomeClass:

Listing 5. Implementacja BussinesInt w SomeClass

public class SomeClass {

	private static class SomeBussinesInt implements BussinesInt {

		@NotNull
		private final Integer someInt;

		public SomeBussinesInt(Integer someInt) {
			this.someInt = someInt;
		}

		@Override
		public Integer getValue() {
			return someInt;
		}
	}

//...
}

oraz SubSomeClass:

Listing 6. Implementacja BussinesInt w SubSomeClass

public class SubSomeClass extends SomeClass {

	private static class SubSomeBussinesInt implements BussinesInt {

		@Null
		private final Integer someInt;

		public SubSomeBussinesInt(Integer someInt) {
			this.someInt = someInt;
		}

		@Override
		public Integer getValue() {
			return someInt;
		}
	}

//...
}

na koniec jeszcze kilka zmian w kodzie SomeClass:

Listing 7. Nowa wersja SomeClass

public class SomeClass {
//...
	@Valid
	private final BussinesInt someInt;

	public SomeClass(Integer someInt) {
		this.someInt = new SomeBussinesInt(someInt);
	}

	protected SomeClass(BussinesInt bussinesInt) {
		this.someInt = bussinesInt;
	}

	public Integer getSomeInt() {
		return someInt.getValue();
	}
}

i jedna bardzo istotna w SubSomeClass:

Listing 7. Nowa wersja SubSomeClass

public class SubSomeClass extends SomeClass {
//....
	public SubSomeClass(Integer someInt) {
		super(new SubSomeBussinesInt(someInt));
	}
}

Ok teraz pytanie co tu się stało?

Dlaczego tak?

Stajemy tu przed często spotykanym problemem dopuszczenia wyjątku jako normalnego zachowania.

W ogólności każdy obiekt SomeClass nie może mieć wartości null w swoim biznesowym polu. Jednak w przypadku SubSomeClass dopuszcza się tą wartość.

Obie klasy są prawidłowo umieszczone w hierarchii, obie zachowują się „na zewnątrz” w taki sam sposób. Inne są jednak zachowania wewnątrz klas. Tu mała uwaga, jeżeli podklasa zmienia zachowanie nadklasy w znaczący sposób np. umożliwia zwrócenie wartości null pomimo, że nadklasa wyraźnie tego zabrania to oznacza, że należy zmienić hierarchie klas. Tu nie ma takiego zagrożenia ponieważ metoda getSomeInt nie mówi nic o ograniczeniu wartości zwracanej więc null jest poprawnym wyjściem. Przykład z życia? Pracownik – handlowcy mają klientów asystenci handlowców nie mają klientów, ale są też handlowcami (nie dostają tylko bonusów z tytułu posiadania klientów).
klasy różnią się wewnętrzną obsługą jednego z pól zatem zamykamy to pole w specjalizowanym interfejsie, która może, ale nie musi być implementowany przez podklasy. Wystarczy zatem, że dodamy implementację tam gdzie tego potrzebujemy. Reszta bez zmian.

podsumowanie

JSR 303 fajne jest, ale ma wiele kruczków-sztuczków (Corvus Corvax Magicus), które czasami przyprawiają o ból głowy.

2 myśli na temat “JSR-303, a spadki

  1. Pierwsza reakcja to krzyk: Co to za kod?! A gdzie LSP i zdrowy rozsądek? Czy aby na pewno dziedziczenie to odpowiednie narzędzie do problemu?

    Ale że już ochłonąłem, powstrzymam się od krzyku i dam szansę: Masz może pod ręką jakiś rozsądny konkretny przykład, gdzie ten problem rzeczywiście się pojawia i taka implementacja jest zasadna?

  2. Problem z życia:
    – w systemie rozliczeniowym poza normalnymi transakcjami są też transakcje „bez odbiorcy”.
    – bank wysyłając taką transakcję do systemu rozliczeniowego nie podaje informacji o odbiorcy.
    – ja wczytuję transakcje z pliku płaskiego (binarny) i muszę je zweryfikować.
    – każda transakcja wygląda tak samo, a rozróżniamy je po „typie komunikatu”.
    – sama „wczytywajka” na podstawie typu tworzy obiekty różnych klas, ale później korzystam już tylko z jednej nadklasy (abstrakcyjnej).

    Chcąc w tym przypadku uniknąć dodatkowego rzutowania i sprawdzania typu tuż przed walidacją po prostu robię taką „magię” w hierarchii klas. Mógłbym użyć co prawda grup, ale:
    – przy dziedziczeniu nie zadziałają
    – są strasznie powolne jak na moje potrzeby.

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