Pewnego dnia pięknego, robiąc przegląd kodu natknąłem się na sygnaturę metody:
public Map<String, Map<String, List<String>>> getProvinces();
Konia z rzędem temu, kto na podstawie takiej sygnatury jest w stanie powiedzieć, co poeta miał na myśli. A cóż ja, jako czytelnik miałem zrobić? Niestety nie dysponowałem źródłami. Testy, debugger do ręki – aż po godzinie odkryłem cóż tam siedzi w środku:
Map<String, Map<String, List<String>>> provinces = getProvinces();
for(String province : provinces.keySet()) {
for(String city : provinces.get(province).keySet()) {
for(String postalCode : provinces.get(province).get(city)) {
log.info(province + " -> "+ city + " -> "+ postalCode);
}
}
}
Wiem że bez pakietu java.util.* trudno cokolwiek sensownego zaprogramować, ale beztroskie korzystanie z takich wielokrotnie zagnieżdżonych struktur danych potrafi zdrowo namieszać.
Nic nie widać, nic nie słychać, za to czuć smród
Bez czasochłonnej analizy implementacji nie wiadomo co siedzi w środku. Gdy taka struktura danych pełni rolę odpowiedzi, to nie jesteśmy w stanie bezpiecznie jej interpretować, zaś jeśli w ten sposób ktoś zaprojektował argumenty metody to sytuacja jest równie nieciekawa. Skazani jesteśmy na empiryczne odkrywanie tego co skrywa przed nami implementacja. Zwykle takie doświadczenie przyjemne nie jest, wszak chcemy tylko skorzystać z kawałka istniejącego kodu by zaoszczędzić sobie czasu, bebechy powinny nas guzik interesować.
Utrata tożsamości
Często widzę kod, mający wyraźnie zarysowane obiekty biznesowe z jasno określoną odpowiedzialności, które gdzieś między kolejnymi warstwami aplikacji miękną i roztapiają się niczym kostka masła pozostawiona na słońcu. Tyle że zamiast tłustej plamy otrzymujemy tłustą Map’ę Map. To znak że albo architektura naszej aplikacji traci swą pojemność, albo że nie mamy odwagi stworzyć kilku prostych ValueObject’ów, drżąc ze strachu przed utratą wyimaginowanej wydajności. Taka mapa jest jak bohater filmu „Pamięć absolutna” – kryje w sobie wspomnienia zepchnięte głęboko do podświadomości.
Koniec z ukrywaniem „oczywistych oczywistości”
To ValueObject’y przychodzą na ratunek. Rezygnując z typów prostych może i stworzymy kilka klas więcej, ale zyski przeważają szalę:
class Province {
public final String name;
public Province(String name) {
this.name = name;
}
}
class City {
public final String name;
public City(String name) {
this.name = name;
}
}
class PostalCode {
public final String value;
public PostalCode(String value) {
this.value = value;
}
}
i możemy zrefaktoryzować naszą przebrzydłą Map’ę:
public Map<Province, Map<City, List<PostalCode>>> getProvinces();
Czujecie różnicę? Taka sygnatura wygląda o niebo lepiej i jest czytelna już na pierwszy rzut oka. Jest jak wzrok przywrócony ślepcowi. Jak erekcja u impotenta. Nikt nie musi zastanawiać się nad strukturą danych – wszystko jest podane jak na tacy. Jedyne o co trzeba zadbać, to poprawne przysłonięcie metod hashcode()/equals() w klasach Province/City/PostalCode. Zwykle w przypadku prostych ValueObject’ów wystarczy standardowe HashCodeBuilder/EqualsBuilder ze stajni apachowych commons-lang:
@Override
public int hashCode() {
return HashCodeBuilder.reflectionHashCode(this);
}
@Override
public boolean equals(Object obj) {
return EqualsBuilder.reflectionEquals(this, obj);
}
Epilog
Uporawszy się z problemem, otarłem pot z czoła i z czystej ciekawości postanowiłem sprawdzić w historii svn’a któż to taki przysporzył mi tyle dodatkowej pracy. Chciałem wiedzieć kogo winić za stracony czas. O ironio! Twórcą metody getProvinces() byłem ja sam, własnoręcznie sobie zgotowałem ten los kilka lat temu. Co po raz kolejny potwierdza że surowa Map’a to kiepski przyjaciel Programisty.