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.