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.