Ekstremalna obiektowość w praktyce – część 2 – Nie używaj słowa kluczowego else

Część 0
Część 1

Drugą część cyklu Ekstremalna obiektowość w praktyce czas zacząć. W głośnikach leci sobie Hammerfall, a my zajmiemy się drugą zasadą Jeff’a Bay’a.

Nie używaj słowa kluczowego else

Przecież to nic trudnego powiecie. Zatem na początek mały przykład kodu, w którym waszym zadaniem będzie zastosowanie tej zasady:

Listing 1. Jak by tu usunąć else

public AbstractServiceStub(Object object) {
		if (service == null) {
			super.service = new Service();
		} else {
			super.service = service;
		}
		((Service) super.service).setTypeMappingVersion("1.1");
	}

To jest kod wygenerowany przez AXIS. Trzeba z niego usunąć słówko kluczowe else. Proste, prawda?

W tym przypadku wystarczy użyć Extract Method w celu wyciągnięcia inicjalizacji do oddzielnego bloku:

Listing 2. Usunięcie else przez Extract Method

private void initService(final Service service) {
		if (Assertions.isNull(service)) {
			super.service = new Service();
			return;
		}
		super.service = service;
	}

Metoda prosta i skuteczna. Życie nie jest jednak takie proste za każdym razem.

Określony przepływ

Trochę bardziej skomplikowane zadanie. Jest sobie obiekt State reprezentujący różne stany konfiguracji – prawidłowy, nieprawidłowy i niezainicjowany. Trzeba zaprogramować prawidłową zmianę stanu, czyli:

  • Ze stanu niezainicjowany można zmienić stan na prawidłowy i nieprawidłowy.
  • Ze stanu prawidłowy można zmienić stan na nieprawidłowy.
  • Ze stanu nieprawidłowy nie można zmienić już stanu.

Oczywistą sprawą będzie użycie enuma. Mniej oczywiste jest zmienianie stanu:

Listing 3. Uzależnienie wartości zwracanej od różnych warunków

package pl.koziolekweb.eowp2;

public enum State {

	VALID, INVALID, NOT_INIT;

	public State changeState(State target) {
		if (target == VALID && this != INVALID) {
			return VALID;
		} else if (target == INVALID) {
			return INVALID;
		} else if (target == NOT_INIT && this == NOT_INIT) {
			return NOT_INIT;
		} else {
			return this;
		}
	}
}

Zaszalałem 😀 Ok, co można tu zrobić? Można oczywiście zamiast konstrukcji else if zastosować kilka ifów, a zamiast ostatniego else zwracać this jako wartość domyślną. wtedy będzie to wyglądało mniej więcej tak:

Listing 4. Uzależnienie wartości zwracanej od różnych warunków bez else

package pl.koziolekweb.eowp2;

public enum State {

	VALID, INVALID, NOT_INIT;

	public State changeState(State target) {
		if (target == VALID && this != INVALID) {
			return VALID;
		}
		if (target == INVALID) {
			return INVALID;
		}
		if (target == NOT_INIT && this == NOT_INIT) {
			return NOT_INIT;
		}
		return this;

	}
}

W sumie nie zmieniło się nic.

Drugi if w metodzie to tak naprawdę ukryty else

Nie ma w tym nic dziwnego. Po prostu formułkę „Jeżeli a to b w przeciwnym wypadku c” rozbijamy na „Jeżeli a to b. Jeżeli nie a to c”. Zysku żadnego, kod niby lepszy, a w praktyce gorszy, bo else choć złe to jednak coś oznacza. Kilka ifów z rzędu tylko gmatwa kod.
W tym konkretnym przypadku rozwiązaniem jest wykorzystanie szmocy… znaczy się wykorzystamy polimorfizmW. Zachowanie będzie zależało od konkretnej implementacji:

Listing 5. Uzależnienie wartości zwracanej od różnych warunków za pomocą polimorfizmu

package pl.koziolekweb.eowp2;

public enum State {

	VALID {
		@Override
		public State changeState(State target) {
			if (target != NOT_INIT) {
				return target;
			}
			return this;
		}
	},
	INVALID {
		@Override
		public State changeState(State target) {
			return this;
		}
	},
	NOT_INIT {
		@Override
		public State changeState(State target) {
			return target;
		}
	};

	public abstract State changeState(State target);
}

Z zestawu kilku ifów wybraliśmy jeden istotny, a resztę usunęliśmy. Nie ma ifów, nie ma elseów udających ify. Ładnie.

Motywacja

Po co tak naprawdę stosujemy tą regułę. Powód jest prosty i nazywa się złożoność cyklomatycznaW (mądre słowo). Jednostką złożoności jest ilość WTF/s. Inaczej mówiąc jest to liczba określająca zagmatwanie kodu. Ściślejsza definicja mówi coś o ilości brzegów, wierzchołków i punktów wejścia i wyjścia w grafie przepływu programu. Takie matematyczne pieprzenie, które nas malo obchodzi na chwilę obecną 😉
Generalnie sama złożoność nie będzie wiele mówić jeżeli programujemy obiektowo. Trochę lepszą metryką jest średnia złożoność na metodę w danej klasie. Jeżeli jednak korzystamy z tej metryki to trzeba pamiętać, że limity podane na wiki są znacznie niższe.

Jak mierzyć CC?

Potrafi to i Cobertura i JDepend i JavaNCSS.

Po co mierzyć CC?

Ponieważ nie tylko else wpływa na zagmatwanie kodu. Jest to dobra metryka obrazująca ogólną złożoność kodu i pozwala w połączeniu z prezentowaną tu regułą na uproszczenie kodu.

Jak stosować ta regułę

Istnieje kilka sposobów na to jak wprowadzić dziś omawianą regułę do kodu. Oto kilka z nich. Nie wszystkie, bo dużo w tym wypadku zależy od konkretnego przypadku, ale to te najpopularniejsze.

Odwróć warunek

Czasami można pozbyć się else odwracając warunek. Zamiast używać == można użyć !=. Trochę bardziej skomplikowaną sztuczką może być tu wykorzystanie prawa De MorganaW choć to czasami wprowadza znacznie więcej zamieszania. Warto tu mieć już gotowy komplet testów, które pokrywają wszystkie kombinacje warunków.

Polimorfizm

Czyli uzależnienie przebiegu programu nie tyle co od warunku co od dostarczonej wersji metody. Bardzo fajna sprawa, szczególnie jak popatrzymy na scalowe Case Classes. Na dłuższą metę jest to bardzo trudne ponieważ zacznie się biegunka klas spowodowana koniecznością dostarczanie nie tylko klas realizujących różne wersje algorytmu w zależności od warunku, ale też odpowiednich fabryk. Tu ratunkiem mogą okazać się wzorce dekorator i builder oraz rozsądna delegacja zadań, które będą choć trochę upraszczać tworzenie klas.

Delegacja do bibliotek zewnętrznych

Każdy zna konstrukcję jeżeli null to NPE. W Bean Validation API 1.1 będzie walidacja argumentów metod. To pozwoli nam na adnotowanie argumentu i nie martwienie się o walidacje lokalne. Podobnie ma się sprawa z metodami takimi jak equals, hashCode i toString, które są niewyczerpanym źródłem elseów.

Programowanie aspektowe

Tak jak wyżej, ale w trochę inny sposób. Niektóre instrukcje można przenieść do aspektu i nie martwić się ich obsługa w kodzie.

Kontrowersyjny Null Object

Na koniec pozostawiłem sobie najbardziej kontrowersyjną metodę wprowadzenia tej reguły. Generalnie znaczna część instrukcji warunkowych sprawdza czy dany obiekt nie jest null. W zamian można wprowadzić wzorzec Null Object. W skrócie jest to użycie specjalnej implementacji klasy, która „robi nic” zamiast zwrócenia null. Upraszcza to znacznie implementację ponieważ nie trzeba sprawdzać warunków. Z drugiej strony należy pamiętać, że jest to wzorzec wirusowy. Oznacza to, że jeżeli dana metoda będzie zwracać jakiś obiekt, to w implementacji Null Object powinniśmy zwracać Null Object danego obiektu. Zatem obok drzewka realnych obiektów otrzymujemy ich nullowe odpowiedniki. Coś za coś.

Podsumowanie

Kolejna stosunkowo prosta reguła. Pozwala ona na tworzenie stabilnego, prostego kodu, który jest bardzo łatwy w testowaniu. Ograniczenie ilości warunków w instrukcjach warunkowych pozwala na uzyskanie lepszego pokrycia testami dla tych instrukcji.
Wadą jest konieczność kombinowania ponieważ czasami ciężko wymyślić jakieś dobre rozwiązanie problemu i trzeba zmieniać wszystkie założenia co do klasy.
Warto jednak wprowadzać tą regułę tak często jak tylko się da ponieważ dzięki temu nasz kod staje się prosty, a ilość WTF/s dąży do 1.

14 myśli na temat “Ekstremalna obiektowość w praktyce – część 2 – Nie używaj słowa kluczowego else

  1. W listingu 4. Checkstyle krzyknie, ze za duzo returnów. Max 3 na metodę.

    Wiadomo mozna wprowadzic zmienna lokalna i do niej przypisywać wartości danego if’a, a na koncu zwrocic jej wartosc.

    Jednak to stwarza niebezpieczenstwo, ze wejdzie do dwoch ifów.

    Czy jest na to jakies rozwiazanie ?

  2. Checkstyle i inne tego typu narzędzia są przydatne, bo szybko wyszukają miejsca wrażliwe. W pracy więcej siedzę nad analizą raportów checkstyle niż nad samą refaktoryzacją. To jest dobry początek.
    Przypisywanie wartości z Ifa do zmiennej jest opcją, ale należy pamiętać, że często if służy do ucieczki z metody w przypadku spełnienia warunków. Czasami oznacza to przekazywanie wartości do Ifa w celu weryfikacji czy odpalać Ifa.
    Jeśli chodzi o niebezpieczeństwo to zobacz pierwszy przykład przez pryzmat dwóch ifów. Drugi if może zamazać działania pierwszego.

  3. Czyli generalnie nie da się ominąć kilku returnów ewentualnie else ifów ? Jeżeli używamy checkstyle.

  4. Nie tyle co się nie da ominąć, bo zawsze da się coś wykombinować, co checkstyle i jeszcze kilka innych narzędzi pozwala na szybkie odnalezienie felernego kodu.

  5. Świetny kurs! Szkoda, że ja tak nie potrafię pisać. Próbowałem i mi partactwo wychodzi, nawet przy prostych rzeczach, a wiedza o jakości produktu wyjściowego i nawet umiejętność wskazania konkretnych partactw mi jakoś nie pomaga. Ale muszę znowu kiedyś spróbować.
    Dużo konkretów, ale pomimo to ma się wrażenie czytania książki popularnonaukowej, na prawdę dobry tekst. Nawet te prześmiewcze wstawki, symbol naszego „solidarnego” społeczeństwa, nie przeszkadzają.

  6. Nie jestem doświadczonym programistą, więc mam nadzieję, że w kontekście posta z 4p odnosnie BMI oraz tego pomożesz mi rozwiać kilka wątpliwości.
    1. else-if – dla mnie jest to naturalne – jeśli jest kilka opcji to blok z else-if oznacza dla mnie jedną ‚rodzinę’ warunków – np. sprawdzająca jedną zmienną – rozumiem, że mimo wszystko jest to błędne rozwiązanie – czyli przykład z listingu 5. da sie zastosowac w kazdym przypadku jak chociazby tym z bmi?
    2. w listingu 2. zastosowales asercje – zwykle porownanie do nulla byloby bledem?

  7. 1. Warunek pociąga za sobą jakąś logikę. Jeżeli jest ona nieskomplikowana to else if nie jest problemem, choć nadal należy go wyeliminować. Widząc jakieś drzewko ifów trzeba patrzeć na nie przez pryzmat działań powodowanych przez poszczególne warunki, a nie warunków samych w sobie. Z punktu widzenia programisty ważniejsze jest prawidłowe działanie „biznesowej” części kodu, a nie samych warunków. Z tego też powodu rozbijamy drabinki ifow na prostsze konstrukcje, które możemy łatwiej testować.

    2. można użyć == null, ale wszystko zależy od tego co chcesz osiągnąć. Odpowiednie nazywanie nawet tak prostych operacji wprowadza do kodu pewną „poezję” pozwalającą na łatwiejsze jego czytanie przez osoby mniej zorientowane.

  8. Przykład:
    mam formularz na stronie, user wpisuje dane, ja je przetwarzam. Kod w dużej części to if, if, if… – if (pole1 != null) { zrob cos z danymi}. Można się pozbyć tych ifów zakładając, że nie chcę by było wymagane uzupełnienie wszystkich pól?

  9. Oczywiście. Jeżeli pola są całkowicie niezależne to można za miast ifów użyć listy walidatorów i wzorca wizytator, przejść po wszystkich polach odpalając odpowiedni walidator, błędy składować w mapie.

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