Typy prymitywne z logiką biznesową wstęp do dobrego kodu

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).

2 myśli na temat “Typy prymitywne z logiką biznesową wstęp do dobrego kodu

  1. Trochę się z tobą nie zgodzę w kilku kwestiach.
    1. Twierdzisz, że samo opakowanie prymitywu/klasy z biblioteki standardowej w klasę związaną semantycznie z domeną aplikacji „nie niesie ze sobą żadnej wartości”. „To jest nieprawdziwa nieprawda”, jak powiedziałaby bohaterka pewnej książki. Bo niezależnie od tego czy nasz wrapper przechowuje jakąś skomplikowaną logikę czy stanowi tylko opakowanie, zawsze ma pewną wartość! Otóż lepiej w kodzie wygląda
    List niż List które gdzieś w nazwie zmiennej / komentarzu musi mieć opisane co w tej liście siedzi. Że już nie wspomnę o cudach takich jak
    Map<String,Map<String,Map>>, które czasem można gdzieś spotkać.
    Już sam fakt podmienienia tych String i Integer na klasy z domeny aplikacji (choćby i ten Pesel) sprawia że taka konstrukcja staje się trochę bardziej czytelna. A jak opakujemy jej poszczególne elementy w jakieś dodatkowe klasy to już w ogóle, bo dostaniemy Map które będzie zupełnie czytelne.

    Warto to mieć na uwadze, szczególnie że często pewne klasy po prostu nie potrzebują żadnej dodatkowej logiki, ot są właściwie tylko strukturami/wrapperami ale mimo to warto ich używać dla poprawy czytelności kodu.

  2. Sama czytelność to nie jest taka prosta sprawa. Rzecz w tym, że niby wszystko jest OK, mamy lepszą komunikację kod-czytający, ale jak przychodzi co do czego to, przy założeniu iz mamy do czynienia z gołym wrapperem, w pierwszym kroku metody mamy jakiś toString czy inną cholerę, która wybebesza nam wrapper. Z kolekcjami jest trochę inaczej i już kiedyś o tym pisałem. One są na tyle specyficzne, że opakowanie ich w coś co udostępnia biznesowy interfejs jest zdecydowanie prostsze i sensowniejsze.

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