Ekstremalna obiektowość w praktyce – część 3 – Opakowuj wszystkie prymitywy i Stringi

Część 0
Część 1
Część 2

Z głośników spokojnie tym razem. Era. Swoją drogą przypominają mi się stare dobre czasy gdy przy „Ameno” przerąbywałem się przez kolejne poziomy w Diablo: Hellfire.
Wspomnienia, wspomnieniami czas jednak zająć się trzecią z zasad Jeff’a Bay’a.

Opakowuj wszystkie prymitywy i Stringi(w klasy o specyficznej dla zastosowania nazwie)

Rozejrzyj się wokoło. Zobacz jak zbudowany jest świat. Szybko stwierdzisz, że świat składa się z obiektów, które wchodzą w interakcje. Tak mniej więcej zaczyna się jakieś 99% kursów dotyczących programowania obiektowego. Pozostałe 1% zaczyna się mniej więcej tak: php jest językiem obiektowym.
Rzecz w tym, że w realnym świecie nie występują liczby, znaki czy stringi. Służą one do opisania jakiś konkretnych przedmiotów są ich cechami i wartościują je według określonego wzorca. W Javie liczby są przedstawione jako typu proste, a autoboxing jest tylko dodatkiem pozwalającym na łatwiejszą pracę z nimi. Swoją drogą powoduje to cholernie wiele problemów jeżeli chcemy używać typów liczbowych w metodach przyjmujących Object i przez przypadek podamy z palca prymitywa. To jednak zupełnie inny temat.

Program opisuje świat

Tak jak w realnym świecie tak i w aplikacji nie powinno używać się typów prostych i stringów. W ich miejsce należy wprowadzać niewielkie klasy reprezentujące atomową jednostkę „czegoś”. Klasa taka powinna mieć znaczenie biznesowe ponieważ reprezentuje pewną małą istotę żyjącą w ramach systemu.

Na początek pewien prosty przykład.

Listing 1. Typ prymitywny i „ciekawy błąd”

package pl.koziolekweb.eowp3;

public class MoneyExample {

	public static void main(String[] args) {
		Konto konto = new Konto();
		print(konto.stan());
		konto.uznaj(10);
		print(konto.stan());
		konto.obciąż(5);
		print(konto.stan());
		konto.uznaj(-3);
		print(konto.stan());
		konto.obciąż(-5);
		print(konto.stan());
	}

	private static void print(int stan){
		System.out.println("Stan konta to " + stan);
	}
}

class Konto {

	private int stan = 0;

	public void uznaj(int wartość) {
		stan += wartość;
	}

	public void obciąż(int wartość) {
		stan -= wartość;
	}
	
	public int stan(){
		return stan;
	}
}

Błąd widać na pierwszy rzut oka. Prymitywne typy danych prowokują do tego typu „ekscesów”. Oczywiście można wprowadzić dodatkowy walidator i bawić się w sprawdzanie czy przekazywana wartość nie jest mniejsza od 0. Jeżeli jednak operacje na danej wartości przeprowadzamy w różnych miejscach w aplikacji warto wprowadzić klasę zamiast typu prymitywnego. Jeff Bay idzie dalej i mówi, iż skoro w świecie realnym nie istnieją typy proste jako samodzielne twory to nie należy ich używać w aplikacji.

Typy prymitywne

Oczywiście trochę inaczej należy traktować typy proste, a inaczej String. Trochę lepsze rozwiązanie:

Listing 2. Typ prymitywny i obejście

package pl.koziolekweb.eowp3;

public class MoneyExampleWithType {

	public static void main(String[] args) {
		Konto2 konto = new Konto2();
		print(konto.stan());
		konto.uznaj(new Kwota(10));
		print(konto.stan());
		konto.obciąż(new Kwota(5));
		print(konto.stan());
		konto.uznaj(new Kwota(-3));
		print(konto.stan());
		konto.obciąż(new Kwota(-5));
		print(konto.stan());
	}

	private static void print(Stan stan) {
		System.out.println("Stan konta to " + stan);
	}

}

class Konto2 {

	private Stan stan = new Stan(0);

	public void uznaj(Kwota kwota) {
		stan.uznaj(kwota);
	}

	public void obciąż(Kwota kwota) {
		stan.obciąż(kwota);
	}

	public Stan stan() {
		return stan;
	}
}

class Kwota implements Wartość<Integer> {

	private final int wartość;

	public Kwota(int wartość) {
		if (wartość 

Przykład działa w tym przypadku całkiem nieźle, ale... no właśnie. Jest sobie interfejs Wartość, który tan naprawdę psuje nam wszystko. Z dwóch powodów. Pierwszy to ujawnia jak trzymana jest implementacja typu prostego w klasie Kwota. Drugi nadal operujemy bezpośrednio na typie prostym. Czas wyciągnąć naszą ulubioną broń, czyli dziedziczenie.

Listing 3. Typ prymitywny i dobre podejście

package pl.koziolekweb.eowp3;

public class MoneyExampleWithExtends {
	public static void main(String[] args) {
		Konto3 konto = new Konto3();
		print(konto.stan());
		konto.uznaj(new KwotaBazowa(10));
		print(konto.stan());
		konto.obciąż(new KwotaBazowa(5));
		print(konto.stan());
		konto.uznaj(new KwotaBazowa(3));
		print(konto.stan());
		konto.obciąż(new KwotaBazowa(15));
		print(konto.stan());
	}

	private static void print(StanKonta stan) {
		System.out.println("Stan konta to " + stan);
	}
}

class Konto3 {

	private StanKonta stan = new StanKonta(0);

	public void uznaj(KwotaBazowa kwota) {
		stan = stan.uznaj(kwota);
	}

	public void obciąż(KwotaBazowa kwota) {
		stan = stan.obciąż(kwota);
	}

	public StanKonta stan() {
		return stan;
	}
}

class KwotaBazowa {

	protected final int wartość;

	public KwotaBazowa(int wartość) {
		if (wartość 

Oczywiście ten kod wymaga jednej rzeczy. Testów (oraz synchronizacji, ale to inna inszość). Tych nie prezentuję, bo są nudne, ale trzeba je napisać. OK, ale co się stało? Przede wszystkim StanKonta reprezentuje kwotę (klasa nazywa się KwotaBazowa, bo siedzi w jednym pakiecie z poprzednimi przykładami i się kompilator burzy). Rozszerzyliśmy jednak zachowanie tej klasy w ten sposób, że poza zawsze dodatnią kwotą ma jeszcze flagę Debet. Flaga ta mogła być typu Boolean, ale... nie można by było wtedy uzyskać efektu wartości oraz metody czyDebet - Boolean jest final. To rozwiązanie ma pewne wady. Przede wszystkim rozwlekło kod. Wymaga też testów, ale... pierwotne rozwiązanie nadal wymaga testów w dodatku jest znacznie bardziej podatne na błędy (spróbujcie wrzucić do stanu konta np. zrzutowany char). Z drugiej strony uzyskaliśmy kod odporny na durne błędy. Łatwy w testowaniu. Jeżeli dodatkowo odpowiednio go popaczkujemy to będzie przenośny pomiędzy projektami.

Typ String

Podobnie jak typy proste typ String nie występuje w naturze z jednym wyjątkiem:

getStringFromObject
getStringFromObject

i tego wyjątku się trzymajmy.

Generalnie typ znakowy jest używany dość często jako nośnik informacji. Począwszy od dupereli typu imię i nazwisko, poprzez hasła, loginy, kończąc na adresach URL czy adresach usług. Przesłanianie tego typu ma jednak dwie zalety.

Po pierwsze pozwala na wprowadzenie dodatkowych walidacji na etapie tworzenia obiektu. Tym samym mamy kontrolę nad tym co tworzymy i nie posypie się np. MalformedURLException czy nie otrzymamy pustego napisu.
Po drugie znacznie łatwiej jest zarządzać kodem mając konkretny typ, a nie enigmatyczny String. Przykłady wymyślcie sobie sami.

Inne typy proste

Że co? Czy w Javie są jeszcze jakieś inne typy proste poza prymitywami(plus ich obiektowe wersje) oraz String? To zależy jak na to spojrzymy. Jeff Bay mówi o typach prostych, ale ja osobiści zalecam dodanie do listy typów prostych takich, które pochodzą z różnych narzędzi. Przykładowo jeżeli tworzymy XMLa i mamy schemę to warto poza walidacją ze schemą rozszerzyć klasy typu Node by uzyskać efekt wstępnej walidacji np. by nie wstawić nieprawidłowej nazw elementu.
Podobnie ma się rzecz z usługami sieciowymi. Tu wartą rozszerzenia jest klasa QName, szczególnie jeżeli wywołujemy usługi w sieci z ograniczonym dostępem. Można wtedy wprowadzić dodatkową walidację by sprawdzać już na wstępie czy dana usługa jest osiągalna. Pozwoli to na kontrolowane wyłapanie baboli.

Podsumowanie

Zasada ta jest dość trudna w zastosowaniu. Wymaga umiejętnego użycia zarówno interfejsów jak i dziedziczenia. Tworząc taki kod warto zadawać sobie pytanie "Czy A to B?". Pozwoli to na uproszczenie kodu i wczesne wyłapanie pewnych abstrakcji. Rozwiązanie tego typu pociąga za sobą pewne niedogodności. Przede wszystkim uzyskujemy prawdziwą enkapsulację. Innymi słowy łatwo wyeliminować tu gettery/settery, co pociąga za sobą spore problemy natury prezentacyjnej. Ten problem poruszę jednak w ostatniej części. Na razie można używać getterów 😀

24 myśli na temat “Ekstremalna obiektowość w praktyce – część 3 – Opakowuj wszystkie prymitywy i Stringi

  1. „Rozejrzyj się wokoło. Zobacz jak zbudowany jest świat. Szybko stwierdzisz, że świat składa się z obiektów, które wchodzą w interakcje. Tak mniej więcej zaczyna się jakieś 99% kursów dotyczących programowania obiektowego. Pozostałe 1% zaczyna się mniej więcej tak: php jest językiem obiektowym.”

    Oplulem monitor kawa 🙂 Wrzucam na fejsa 😉

  2. I dlatego właśnie nienawidzę Javy i całego programowania aplikacji klasy ‚enterprise’ – dużo pisania, a przyrostu funkcjonalności nie widać…

  3. @Łukasz, oczywiście do momentu, aż nie trzeba czegoś poprawić. Wtedy takie „dużo pisania” ratuje skórę, bo zmiany są minimalne i nie trzeba testować połowy systemu by sprawdzić czy aby na pewno coś się nie popsuło.

  4. Ale opakowanie i tak opakowanych już typów prostych to programistyczna herezja. Sprzęt zaczyna płakać…

    Im mniej kodu, tym mniej testów. (warto)

    Na prawdę nie warto zastanowić się nad jedną strukturą danych, jedną funkcją + kilkoma testami? Zamiast kilkudziesięciu plików, które: trzeba dokumentować, pokryć testami i nimi zarządzać. A skoro rzucamy klasami w takich drobnych sprawach to się boję jak średniej wielkości aplikacje będzie się pisać…

  5. Może lekko źle to ująłem. Nie kolejne poziom abstrakcji typów prostych(wtf…), a kolejny branch podobnego podejścia. Ot takie magiczne java.lang.Integer sobie istnieje. Co już powoduje czasem drgawki jak się to widzi. Czytając o kolejnych pomysłach mnożenia kodu do N klas, aż się przypominają takie fajne rzeczy z Javy.

    Ja tylko współczuję ludziom, którzy muszą taki kod jaki opisujesz muszą pokrywać testami.

  6. Hmm, jak dla mnie, to już jednak zakrawa na bloated abstraction design. Nie mówię, że nie będzie istniała sytuacja, że takie coś się nie przyda… ale czy to naprawdę jest tego warte? W Twoim ostatecznym, dobrym kodzie mamy 2 poziomy abstrakcji (czy generalnie – dwa opakowania, jeśli abstrakcja jest w tym momencie złym słowem) na inta – opakowując wszystko wielokrotnie, tworząc dobry do przetestowania kod, przerzucając się design patternami, które pięknie wyglądając i pisząc eseje dokumentujące kod… tracimy tak naprawdę sporo czasu, a projekt nie jest gotowy. Książkowy design, a świat rzeczywisty to spora różnica.

  7. @Szymon, nie załapałeś tego DLACZEGO należy dodatkowo opakować typy proste. Chociażby najprostszy przykład z tekstu. Typ Money nie może przyjmować wartości ujemnej. Możesz z tego zrezygnować, ale:
    – i tak musisz pokrywać testami kod, który sprawdza czy wartość z money nie jest ujemna.
    – money idzie przez cały flow aplikacji i nie trzeba na każdym kroku sprawdzać czy aby na pewno nie masz wartości ujemnej.
    – kod jest opakowany w metody biznesowe. Nie pracujesz już z typami prostymi, a z obiektami. To pozwala uniknąć „czeskich bugów” gdy ktoś np. poda zamiast liczny z przedziału 1-10 liczbę z poza tego.

    @Łukasz, tylko, że w przykładach masz wycinek rzeczywistości. W większym systemie będzie się to sprawdzać. Ja w rzeczywistości cierpię katusze użerając się z Stringowym typem jako identyfikatorem banku, który jest w praktyce intem (taka zaszłość). W nowej aplikacji stworzyłem typ BankId i mówiąc szczerze odpadają mi problemy czy identyfikator jest przysłany jako 001 (Strign), czy 1(int). Cała logika zamknięta w jednej klasie i dalej już nie muszę sprawdzać co mi przysłali. Dodatkowo mogłem to opakować we własny typ dla SQL (tu mamy varchar(3)) i nawet nie muszę kombinować z budową zapytań.

    Na koniec ważna rzecz. To są przykłady. W rzeczywistym systemie wykorzystuję te zasady od pewnego czasu i po początkowych trudnościach np. z dokumentacją czy testami obecnie piszę się szybko i przyjemnie. Eliminacja tych trudności polega na dobrym wykorzystaniu IDE. W praktyce w czasie pracy nie dotykam myszy. Nie muszę. Ma zdefiniowane szablony kodu, skróty klawiaturowe i makra. Jak dla mnie to jest bardzo wygodne.

  8. Oj nie. Bo właściwie to nie jest konieczne;). To tylko obszerne rozwiązanie tego problemu.

    Powód dlaczego należy podjąć jakąś decyzję co to sytuacji z Money jest klarowny. Jednak w proponowanym przypadku:
    1. Dodatkowy zbędny kod.
    2. Dodatkowe zbędne testy pokrywające dodatkowy zbędny kod.

    Analogiczny rezultat osiągniesz przez prostą, jedną funkcję, do której napiszesz jeden prosty test. A sam clipping wartości, warunków – do tego wszystkiego są inne, ciekawe alternatywy, o których polecam przeczytać.

    Bo tu mamy już OOP hell. Czasem warto się oderwać na chwilę od metod proponowanych przez takich ludzi i włączyć myślenie. Czy nie ma innych, lepszych metod.

    Tak jak wspomniał

  9. Walidacja jest potrzebna tylko na styku aplikacji z wejściem/wyjściem – czyli w UI oraz w interfejsach. Implementacja tego typu rozwiązania jest ekstremalnym przegięciem zastosowań OOP. Coś takiego w programowaniu funkcjonalnym jest nie do pomyślenia. Dla kontrastu proponuję zainteresować się właśnie FP, a także spojrzeć do genialnej prezentacji dotyczącej Data Oriented Design – gdzie niestety tego typu podejście zostało bezlitośnie skrytykowane. http://warsztat.gd/articles.php?x=view&id=409

  10. Jedną prostą funkcję powiadasz? Tylko gdzie ją wstawić? Przenieść do klasy narzędziowej? I tak będzie trzeba sprawdzać tą wartość w każdym miejscu przez, które przechodzisz. Tu właśnie pozbywasz się zbędnego kodu rozsianego po wielu miejscach w projekcie. Ustalasz prostą konwencję – jest obiekt Money. Koniec kropka. Nie powinno cię interesować co jest w środku, jak to jest napisane i przetestowane.
    Problemem w wielu projektach polega na tym, że nie potrafimy się oderwać do projektu jako całości. Wystarczy, że postawisz sobie zadanie pt. „Napisać klasę Money i zdobyć władzę nad światem”. Dzięki temu później nie musisz martwić się o to CO zawiera ta klasa, czy ma testy i dokumentację developerską. Po prostu jest i spełnia pewne założenia określone w dokumentacji użytkowej. Traktujesz ją jako kolejną bibliotekę.

  11. @lamer_nie_programer, tyle tylko, że tworząc kod uwzględniając cały system (m.n. to, że walidacja odbywa się tylko na poziomie przekazania danych z UI) powodujemy, że nie można go użyć gdzieś indziej. Okazuje się, że chcą użyć danego kodu w innym projekcie musimy dokładać kolejne walidatory. W prezentowanym przeze mnie podejściu walidacja odbywa się tylko raz – w momencie tworzenia obiektu. I tylko tam. To czy obiekt tworzony jest przez jakiś mechanizm mapujący dane z UI i przekazujący go dalej czy też tworzymy ten obiekt w np. DAO nie ma znaczenia. Sprawdzenie poprawności danych jest zadaniem wewnętrznym danej klasy. To jak jest ono realizowane nie jest dla nas istotne.
    Przykład z prezentacji (Koło i elipsa) pokazuje tylko, że panowie nie za bardzo potrafią programować obiektowo 🙂 Dobre programowanie obiektowe jest sztuką i jej opanowanie jest naprawdę trudne. Tworząc hierarchę dziedziczenia powinno oddawać się hierarchię z rzeczywistości (dlatego koło to elipsa, a kwadrat to prostokąt – inne podejście to błąd! i to na poziomie logicznym niezależnym od wybranej metodyki).
    Ciekawie przedstawione wady getterów i setterów, ale one nie są potrzebne. Generalnie i w ogóle. Zresztą w prezentacji poruszane są inne złe nawyki programowania obiektowego. Zasady z tego cyklu prezentują te problemy i mówią jak je można rozwiązać.
    I na koniec przykład z „Magicznym rondem” – umiejętność jazdy po rondzie jest trudna. Tak samo jak programowanie obiektowe. Zresztą:
    http://www.roundabout.net/swindonRAB.jpg jak widać można programować jak n00b można jak pr0.

  12. Ja mam jeszcze jeden problem z pięknym designem obiektowym – wygląda on dobrze w przykładach – a w produkcji? Pokazałeś jak rozmnaża się jedna klasa, na kilka innych i wszystko jest pięknie – ale ten przykład jest mimo wszystko sztuczny, bo oderwany od prawdziwego projektu. Kilka razy widziałem piękny design klas.. z którym się wiązał tylko jeden problem – wyjątki i ich obsługa. Nie dane było mi ujrzeć jeszcze, żeby ktoś połączył dobrą archietekturę z dobrym pomysłem na wyjątki. A to tylko kropla w morzu – dobry podział na pakiety/przestrzenie nazw, to kolejne TRUDNE zagadnienie. Jest ich jeszcze parę.

    Często mam wrażenie, że z obiektowością jest jak z demokracją – wszyscy używają, bo nie ma (w ich uznaniu) nic lepszego (pozdrowienia dla programistów języków funkcyjnych ;)). Idealna realizacja paradygmatu obiektowego, tak aby nikt nie ucierpiał jest moim zdaniem niemożliwa.

  13. @Łukasz, na co dzień spotykam się z trochę inną klasą problemów. Tam mam do dyspozycji pliki, które mają od około 1500 do nawet 25000 linii kodu. Bez dokumentacji. Jednocześnie nowe fragmenty są dopisywane w oparciu o zasady OOP i zazwyczaj znacznie lżejsze i mniejsze.
    Co do obsługi wyjątków to należy pamiętać, że Java jest bardzo specyficzna pod tym względem. W przytoczonej przez @lamer_nie_programer, prezentacji (w C/C++) autorzy podchodzą do tego na zasadzie „olać to ciepłym moczem”. Jest to jakaś metoda. Można jednak wykorzystać pewne elementy czy to języka czy różnych specyfikacji i rozszerzeń do stworzenia jednolitego sposobu obsługi wyjątków. Tu kłania się zarówno programowanie aspektowe jak i Bean Validation.
    Co zaś tyczy się programowania funkcyjnego to było ono przyszłością, jest przyszłością i będzie przyszłością IT. Choć bardzo dobre to jednak w większości przypadków jest zbyt drogie.

  14. Z ciekawości – co masz na myśli mówiąc drogie? Bo zapewne nie chodzi Ci o wydajność – Haskell jest kompilowany do postaci binarnej wykonywalnej 🙂

  15. Drogie w sensie stawek dla programistów. Nie zapominaj, że poza tworzeniem kodu trzeba go jeszcze utrzymać. Programowanie funkcyjne ma niestety to do siebie, że wymaga dość dobrze opanowanego warsztatu nawet wtedy gdy mamy do czynienia z prostymi zadaniami.
    To powoduje, że do wsparcia i utrzymania trzeba by zatrudnić osoby o co najmniej dobrym poziomie.

    W przypadku programowania obiektowego nie ma czegoś takiego. Tu obowiązuje zasada w myśl, której każdy diagram UML można zastąpić skończoną ilością studentów.

  16. Koziołku Drogi i Wielce Szanowny,

    z przykrością stwierdzam żeś tym razem tryknął łbem w obiektową ścianę i nabiłeś sobie guza jak stąd do Honolulu. 🙂

    Sorry, ale koszmarny kod jaki wyprodukowałeś w miarę zastępowania prymitywów obiektami woła o pomstę do nieba. 🙂 Przeciążone konstruktory, generyki, dziedziczenie… nie kupuję tego.

    Taka dygresja – Erich Gamma wspominał że po opublikowaniu książki o design patterns dostawał maile typu „w naszym projekcie udało się nam użyć 23 z wzorców jakie podajesz, ale nie wiemy jak umieścić tam 24-ty. Czy mógłbyś pomóc?” Mam poczucie że kod jaki proponujesz powiela ten typ myślenia.

    Fakt że rzeczywisty świat składa się z obiektów wcale nie oznacza że programując musisz mieć na wszystko obiekt. Program jest pewną abstrakcją (uproszczeniem) świata. I prymitywy fantastycznie nadają się do tegoż uproszczania, dzięĸi czemu łby nam nie puchną, i jesteśmy w stanie różne złożone zagadnienia przekuwać w kod.

    Dla przypomnienia:
    – KISS
    – every line of code not written is correct, or at least guaranteed not to fail


    pozdrawiam
    Tomek Kaczanowski

  17. @Tomku, zgodzę się, że druga wersja jest fe. To tylko proteza. Pamiętaj jednak, że chęć ucieczki od typów prostych ma swoje uzasadnienie. Na pewnym poziomie łatwiej posługiwać się obiektem Kwota niż niewiele mówiącym intem czy o zgrozo doublem. Podobnie rzecz ma się ze stringami. Sam kilka razy złapałem się na parsowaniu stringa tylko dlatego, że nie chciałem wprowadzać klasy i pisać testów. Wynik był taki, że miałem więcej roboty ze stringiem ciągnącym się przez kilka warstw zamiast już na początku zamienić go na jakiś konkretny obiekt.

  18. Wszystko fajnie, ale doprowadziłeś to do absurdu i prawde powiedziawszy wygląda to na kpiny a nie realną propozycję sposobu w jaki pisać kod. Wprowadzanie typów może być ok, byle nie iść za daleko. Klasyczny artykuł Fowlera (http://martinfowler.com/ieeeSoftware/whenType.pdf) dość dobrze to wyjaśnia.


    pozdrawiam
    Tomek

  19. Dzięki za artykuł. Generalnie jest tu trochę inny problem. Przykłady mają to do siebie, że są stosunkowo małe. Zatem wprowadzanie typu rzeczywiście mija się z celem. Warto jednak zwrócić uwagę na to co Fowler pisze na końcu. Wprowadzenie typu musi nastąpić w odpowiednim momencie.
    Na początku cyklu zwróciłem uwagę, że zasady Bay’a są dobrym punktem zaczepienia jeżeli chcemy refaktoryzować kod. Ich konstrukcja uwypukla słabe miejsca, a w tym konkretnym przypadku pozwala na odnalezienie bytów „biznesowych”, które w swojej prymitywnej formie mogą okazać się kłopotliwe.

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