Większość aplikacji obiektowych, które są tworzone jak świat długi i szeroki nie ma za dużo wspólnego z OOP. Przynajmniej na poziomie modelu. Ten został w czasach proceduralno-strukturalnych. Mamy zatem klasy, które w rzeczywistości są strukturami znanymi z C tyle tylko, że wyposażonymi w gettry i settery.
Te klasy są trochę jak aktorki w filmach porno. Patrzysz na nie i niby mają na sobie fatałaszki (metody), ale jak zaczniesz z taką zabawę to dość szybko okaże się, że nie ma ona nic do ukrycia. O ile w porno jest to OK, to w programowaniu obiektowym nie do końca. Klasa, która ujawnia zbyt dużo może szybko stać się źródłem problemów. W dodatku sama w sobie ma tendencję do obrastania dodatkowymi klasami narzędziowymi, bez których nie za bardzo może żyć.

Friendzone

Jest to pewien antywzorzec będący bezpośrednim następstwem anemicznego modelu. Można powiedzieć, że klasa „awansowała” do friendzone jeżeli w stosunku do innej klasy pełni rolę służebną nie otrzymując nic w zamian. Klasa taka musi spełniać pewne wymagania:

  • Musi być częścią publicznego API systemu.
  • Jej usunięcie nie ma wpływu na logikę działania systemu.
  • Jej usunięcie ma wpływ na zachowanie systemu.

Założenia te spełnia większość klas typu „utilsy do czegoś tam”. Szczególnie zaś najprzeróżniejsze parsery informacji z GUIa, serwisów zewnętrznych.

Przypadek specjalnej troski

Specyficznym przypadkiem jest użycie typów podstawowych (prymitywnych i String) jako pełnoprawnych członków modelu. To zły pomysł. Jak dopuścisz prymitywów do dyskusji to zaczną odpierdalać nietoperza, osrają wszystko dookoła, a na koniec dadzą w mordę przypadkowym gościom. Jeszcze gorzej jest gdy typ prymitywny niesie w sobie pewną logikę biznesową. W takim wypadku trzeba z nim jakoś żyć. Niestety wielu programistów idzie po linii najmniejszego oporu i zamiast opakować typ prymitywny „friendzonuje” grupę klas by tylko się nie narobić. A to błąd…

Karmimy model

By jakoś wyrwać się z okowów anemicznego modelu należy przeprowadzić refaktoryzację. Zazwyczaj pierwszy krok takiej refaktoryzacji polega na wydzieleniu klasy opakowującej. Niestety wiele osób na tym kończy. W najlepszym wypadku dostajemy takiego potworka jak poniżej

Listing 1. Ta klasa też jest anemiczna

public class Pesel {

	private final String pesel;

	public Pesel(String pesel){
		this.pesel = pesel;
	}

	@Override
	public String toString() {
		return pesel;
	}
	public void accept(Validator validator){
		validator.validate(this.pesel);	
	}

}

Niby wszystko jest OK. Nawet jakaś walidacja jest… taka to wali wyjątkami… Ale jednak trochę do dupy… trochę bardzo.

Klasa nie niesie ze sobą żadnej wartości. Jak ją przerobić na coś co ma wartość? Wszystko zależy od tego jaką informację biznesową niesie ze sobą określony typ prymitywny. W przypadku numeru PESEL ilość informacji jest stosunkowo duża jak na tak krótki numer. Zatem można wygenerować dość bogatą klasę opakowującą. W dodatku ze względu na specyfikę logiki numeru PESEL można tu naprawdę dużo upchnąć.

Pierwszym krokiem przy dokarmianiu modelu jest decyzja jak „bogaty” ma być. Ja postaram się to zrobić na full wypas. Zatem klasa będzie niosła ze sobą całą logikę zawartą w numerze PESEL oraz dodatkowo będzie kompensować jego niedociągnięcia.

Listing 2. Pesel na bogato pierwsze kroki

public class RitchPesel {

	private final String dateOfBirth;
	private final String serialNumber;
	private final int sexDigit;
	private final int controlDigit;

	private final boolean isCorrect;
	private final boolean isValid;

	private RitchPesel(String dateOfBirth, String serialNumber,
	                   int sexDigit, int controlDigit,
	                   boolean isCorrect, boolean isValid) {
		this.dateOfBirth = dateOfBirth;
		this.serialNumber = serialNumber;
		this.sexDigit = sexDigit;
		this.controlDigit = controlDigit;
		this.isCorrect = isCorrect;
		this.isValid = isValid;
	}

	public Sex sex() {
		return Sex.byDigit(sexDigit);
	}

	public LocalDate birthDate() {
		return PeselBirthDateParser.parse(dateOfBirth);
	}

	public boolean isCorrect() {
		return isCorrect;
	}

	public boolean isValid() {
		return isValid && isCorrect;
	}

	@Override
	public String toString() {
		return dateOfBirth + serialNumber + sexDigit + controlDigit;
	}

}

Zatem mamy coś takiego. Klasa zawiera pewną logikę. Na podstawie numeru PESEL możemy określić datę urodzenia oraz płeć. Na uwagę zasługuje tu rozdzielenie pól isValid i isCorrect. Wynika ono z uwarunkowania historycznego numeru PESEL. Nie każdy prawidłowy numer jest poprawny. Pierwsze pole przechowuje informację o poprawności numeru w sensie wyliczenia sumy kontrolnej. Drugie przechowuje informację o prawidłowości numeru w odniesieniu do rejestru. Oznacza to tyle, że może istnieć numer nie spełniający zasad wyliczania sumy kontrolnej, z nieprawidłową datą bądź nieprawidłowym oznaczeniem płci i będzie on prawidłowy.
O takich sytuacjach zapominają ludzie implementujący obsługę numeru i później jest jazda gdy przychodzi klient, podaje dowód osobisty, a system się burzy.
Kolejna ważna rzecz w tym konkretnym przypadku to prywatny konstruktor. Po co on nam? W końcu można by było stworzyć konstruktor, który przyjmował by numer PESEL jako String i robił odpowiednią magię. Rzecz w tym, że tu znowu daje znać o sobie sprawa poprawności i prawidłowości numeru. Można co prawda dodać kolejny parametr w konstruktorze, który będzie opisywał jak ścisła ma być walidacja, ale to nadal nie rozwiązuje problemu. Taki kod choć krótki nie niesie ze sobą informacji co tak naprawdę tam się w środku dzieje.
Kolejna sprawa, i kolejny parametr w konstruktorze, to walidacji płci. Po samym numerze nie rozpoznamy czy zawarta w nim płeć jest poprawna. Trzeba dorzucić parametr informujący czy mamy do czynienia z mężczyzną czy z kobietą. Podobnie ma się sprawa z datą urodzenia. Tu mamy dwa przypadki. Pierwszy jak w przy płci weryfikacja w źródle zewnętrznym, a drugi to weryfikacja dat w rodzaju 31 listopada. Zatem kolejny parametr, który jest opcjonalny (bo przy wyłączonej ścisłej walidacji można go olać). I tak oto otrzymujemy całkiem sporą grupę konstruktorów, które będą dość zagmatwane w swojej naturze.
Rozwiązaniem problemu jest oczywiście stworzenie budowniczego dla naszej klasy. Na tym etapie możemy zagrać sobie na typach i stworzyć kod w oparciu o dziedziczenie. UWAGA! Co co chcę tu przekazać to dobry przykład poprawnego użycia dziedziczenia. Jeżeli ktoś mówi wam, że dziedziczenie w stylu Figura > Prostokąt > Kwadrat jest ok, to mu nie wierzcie. To jest dobry przykład na implementację interfejsu, ale nie dziedziczenie!
Na początek dodajmy do naszej klasy dwie metody statyczne zwracające nam odpowiednich budowniczych

Listing 3. Dwóch różnych budowniczych

public static Builder getStricBuilder(String pesel){
	return new StrictBuilder(pesel);
}

public static Builder getTolerantBuilder(String pesel){
	return new TolerantBuilder(pesel);
}

To już dużo wyjaśnia. Metody te pomimo swojej prostoty są bardzo ekspresyjne. Zarówno osoba czytająca kod w jak i to używająca nie będzie miała problemu ze zrozumieniem co tu się dzieje. Sama klasa Builder jest klasą wewnętrzną i abstrakcyjną. Nie jest interfejsem ponieważ chcemy w niej przechowywać numer w postaci ciągu znaków. Poza tym będzie ona zawierać wspólną logikę dla obu typów budowniczych.
Kolejnym krokiem jest implementacja budowniczych w oparciu o przekazywane parametry i ustawiane flagi.
Ostatni krok to posprzątanie kodu wedle własnych potrzeb.

Oczywiście należy pamiętać o testach.

Podsumowanie

Samo obudowanie typu prymitywnego w mającą biznesowe uzasadnienie klasę jest stosunkowo proste. Jednak dopiero wzbogacenie takiej encji o logikę biznesową, która jest z nią bezpośrednio powiązana pozwala na tworzenie dobrego kodu. Kod taki będzie pozbawiony publicznych klas, które „wpadły w friendzone”. Zostaną one zamknięte w ramach klasy, którą wspierają.
Kod taki pozwala też na napisanie lepszych testów jednostkowych. Testy takie będą rzeczywiście sprawdzać zachowania, a nie powtarzać logikę biznesową. Warto tu jednak zwrócić uwagę na ilość kodu zamkniętego w encji. Jeżeli będzie go za dużo to testowanie może okazać się mordęgą. Szczególnie jeżeli cały kod będzie w ramach klas wewnętrznych BEZ możliwości dobrania się do nich.
W takim wypadku należy pójść na pewien kompromis. Na pewno część kodu (zazwyczaj pewne klasy narzędziowe) będzie miała dużą wartość też poza klasą, w której została zdefiniowana. Warto zatem upublicznić ich API, a w skrajnym przypadku przenieść poza encje. Świetnym przykładem tego typu klas są walidatory. Jeżeli tylko napiszemy je w miarę ogólnie, na przykład przyjmując, że na wejściu otrzymują dane w formie typów prymitywnych, to nic nie stoi na przeszkodzie by wydzielić je do osobnego pakietu. W takim wypadku, walidator może pracować z różnymi encjami biznesowymi reprezentującymi ten sam byt (bo chyba nie wierzycie w bajki o spójnych encjach biznesowych w projektach integracyjnych).