Ekstremalna obiektowość w praktyce – część 1 – tylko jeden poziom zagłębienia na metodę

Część 0

Witam w pierwszej części cyklu Ekstremalna Obiektowość w Praktyce. Z głośników płynie sobie Another Brick on the Wall part II, a my zajmiemy się pierwszą z zasad Jeffa Baya. Brzmi ona

Tylko jeden poziom zagłębienia na metodę

Jest to jedna z prostszych zasad, której wprowadzenie wymaga tylko umiejętnego wykorzystania refaktoryzacji typu Extract Method. Pytania:

  • Gdzie w Twoim ulubionym IDE jest dostępna ta refaktoryzacja?
  • Jaki jest jej skrót klawiaturowy?

Mam nadzieję, że odpowiedziałeś/aś poprawnie na oba pytania i to bez chwili zastanowienia. Dlaczego to takie ważne? Pisałem o tym w części zerowej, ale przypomnę. Zasady przedstawione w tym cyklu wymagają dobrego warsztatu pracy tak by nie tracić czasu na szukanie narzędzi, a korzystać z nich.

Na czym to polega?

Załóżmy, na początek, że potrzebujesz wypełnić trójwymiarową tablicę za pomocą kolejnych liczb naturalnych, a następnie wypisać ją na standardowe wyjście. Wiem, że przykład jest głupi i mało rzeczywisty, ale bardziej namacalne za chwilę. Program realizujący taką funkcjonalność będzie wyglądał mniej więcej tak:

Listing 1. Wypełnienie i wypisanie tablicy 3D

package pl.koziolekweb.eowp1;

public class Array3DFiller {

	public static void main(String[] args) {
		int[][][] array3d = new int[3][3][3];
		int current = 0;
		for (int i = 0; i 

Realizacja w main tylko dla wygody uruchamiania. Z tego powodu dalej będę używał metod statycznych.

Jak widać jest to niezbyt ciekawy, zagmatwany kod. Tu o tyle prosty, że w poszczególnych pętlach niewiele się dzieje. Pierwsza z reguł Bay'a mówi, że należy tak pisać kod by w metodzie był tylko jeden poziom wcięcia. Co to oznacza? Oznacza to, że masz prawo zrobić 4 spacje/1 tab w celu wcięcia kodu metody, a następnie kolejne 4 spacje/1 tab w celu wcięcia dla np. instrukcji warunkowej czy pętli. W tej wciętej instrukcji nie możesz używać dalszych wcięć. Na listingu 1 komentarze oznaczają poszczególne poziomy wcięć.
Spróbujmy teraz wyekstrahować poszczególne metody tak by osiągnąć kod zgodny z założeniami. W tym celu będziemy używać refaktoryzacji Extract Method.

Listing 2. Kod po pierwszej refaktoryzaci

package pl.koziolekweb.eowp1;

public class Array3DFiller {

	public static void main(String[] args) {
		int[][][] array3d = new int[3][3][3];
		int current = 0;
		for (int i = 0; i 

Jak widać kod wygląda już zdecydowanie lepiej. Nie jest może idealny, ale jest znacznie bardziej czytelny. Możemy oczywiście pogłębić refaktoryzację wyciągając jeszcze pętle z main do osobnych metod:

Listing 3. Kod po drugiej refaktoryzaci

package pl.koziolekweb.eowp1;

public class Array3DFiller {

	public static void main(String[] args) {
		int[][][] array3d = new int[3][3][3];
		int current = 0;
		current = fillArray(array3d, current);

		current = printArray(array3d, current);

	}

	private static int fill2dArray(int[][][] array3d, int current, int i) {
		for (int j = 0; j 

Co dzięki temu uzyskaliśmy? Już po pierwszej refaktoryzacji można zauważyć, nadmiarową część kodu. Jaką? Przyjrzyjmy się wypisywaniu tablicy... po co nam zmienna current? Oczywiście kod tworzony za pomocą kopiuj wklej jest zawsze podatny na tego typu potknięcia. Nie zmienia to jednak faktu, że w pierwotnej wersji kodu umyka ten szczegół. Co ważne, zmienna ta nie jest też potrzebna w metodzie main można zatem przenieść ją głębiej w kod. Dzięki temu osiągamy też ukrycie implementacji przed klientem. Klient nie musi wnikać jak wypełniana jest tablica. W szczególności nie obchodzą go zmienne wykorzystywane przez metodę wypełniającą.
W wyniku wszystkich tych kroków otrzymujemy poniższy kod.

Listing 4. Kod zakończeniu pracy

package pl.koziolekweb.eowp1;

public class Array3DFiller {

	public static void main(String[] args) {
		int[][][] array3d = new int[3][3][3];
		fillArray(array3d);

		printArray(array3d);

	}

	private static int fill2dArray(int[][][] array3d, int current, int i) {
		for (int j = 0; j 

Nie jest to oczywiście kod idealny. Metody mają za dużo parametrów, a klasa ma kilka odpowiedzialności (wypełnienie i wypisanie). Jak go ulepszyć będę jeszcze pisał w następnych częściach. Na chwilę obecną osiągnęliśmy wyznaczony cel. Metody nie mają więcej niż jeden poziom wcięcia.

A może coś bardziej życiowego?

Przykład był mało życiowy, ale prosty. Czas na coś bardziej życiowego.
Jak wspomniałem wcześniej realizowałem ostatnio niewielki projekt, który postanowiłem napisać zgodnie z tymi zasadami. Jedną z funkcjonalności było przepisanie odpowiedzi serwera z tekstu (XML) na kolekcję obiektów. Sprawa prosta, a nie chciało mi się szukać biblioteki mapującej. Zresztą powoli robi mi się w projekcie jar-hell więc dołączanie biblioteki tylko dla jednej metody traci sens.
W uproszczeniu cały program wyglądał by mniej więcej tak

Listing 5. Kod mapowania XML - Obiekt

package pl.koziolekweb.eowp1;

import java.io.ByteArrayInputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

public class TransactionUnmarshaller {

	public static void main(String[] args) throws Exception {
		String transaction1 = "" + "1"
				+ "OK" + "100"
				+ "101" + "";
		String transaction2 = "" + "2"
				+ "OK" + "101"
				+ "100" + "";
		String response = "" + transaction1 + transaction2
				+ "";

		Unmarshaller unmarshaller = new Unmarshaller();
		Collection unmarshallFromStringXml = unmarshaller
				.unmarshallFromStringXml(response);
		System.out.println(unmarshallFromStringXml);
	}

}

class Transaction {
	int id;
	String state;
	long senderId;
	long reciverId;

	@Override
	public String toString() {
		return "Transaction [id=" + id + ", state=" + state + ", senderId="
				+ senderId + ", reciverId=" + reciverId + "]";
	}
}

class Unmarshaller {

	Collection unmarshallFromStringXml(String response)
			throws Exception {
		DocumentBuilder newDocumentBuilder = DocumentBuilderFactory
				.newInstance().newDocumentBuilder();
		Document responseAsXml = newDocumentBuilder
				.parse(new ByteArrayInputStream(response.getBytes()));
		NodeList transactionNodes = responseAsXml
				.getElementsByTagName("transaction");
		int length = transactionNodes.getLength();
		List transactions = new ArrayList(length);
		for (int i = 0; i 

Klasa Unmarshaller to istne pobojowisko. Oczywiście to działa poprawnie i jest nawet szybkie. Tyle tylko, że w czasie testów wyszły nam pewne bugi i poszukiwanie źródła było dość uciążliwe. W pierwszym odruchu refaktoryzacja w duchu zasady Jeff'a Bay'a przebiegła bardzo sprawnie i w jej wyniku otrzymałem coś takiego:

Listing 6. Kod po szybkiej refaktoryzacji

class Unmarshaller {

	Collection unmarshallFromStringXml(String response)
			throws Exception {
		DocumentBuilder newDocumentBuilder = DocumentBuilderFactory
				.newInstance().newDocumentBuilder();
		Document responseAsXml = newDocumentBuilder
				.parse(new ByteArrayInputStream(response.getBytes()));
		NodeList transactionNodes = responseAsXml
				.getElementsByTagName("transaction");
		int length = transactionNodes.getLength();
		List transactions = new ArrayList(length);
		for (int i = 0; i 

Kod stał się czytelniejszy, a i przetestować było go łatwiej. Jednak dzięki temu uzyskujemy dodatkowy bonus. Otóż widać, że część kodu powtarza się. Można zatem pozbyć się tego powtórzenia do osobnej metody.

Listing 7. Kod po dokładniejszej refaktoryzacji

class Unmarshaller {

	Collection unmarshallFromStringXml(String response)
			throws Exception {
		DocumentBuilder newDocumentBuilder = DocumentBuilderFactory
				.newInstance().newDocumentBuilder();
		Document responseAsXml = newDocumentBuilder
				.parse(new ByteArrayInputStream(response.getBytes()));
		NodeList transactionNodes = responseAsXml
				.getElementsByTagName("transaction");
		int length = transactionNodes.getLength();
		List transactions = new ArrayList(length);
		for (int i = 0; i 

Jak widać kod stał się jeszcze prostszy. Oczywiście to nie był koniec prac nad nim i tak jak w poprzednim przykładzie jeszcze wrócę do niego przy omawianiu innych zasad. Na teraz jednak wystarczy tej zabawy. Mamy dobry punkt wyjścia do kolejnych prac.

Czas na małe podsumowanie

Zasada jednego poziomu wcięcia na metodę jest bardzo prosta do zastosowania. Nie wymaga dużego nakładu pracy, a efekty są zadowalające i uzyskiwane bardzo szybko.

Zalety
  • Prostota - zasada prosta do zachowania. Nie wymaga dużo czasu i nakładu pracy.
  • Rozsądna granulacja kodu - po zastosowaniu metody pracują na swoim "poziomie" bez wnikania w niższe i wyższe poziomy. Tak jak w pierwszym przykładzie metody pracujące na wierszu tablicy pracują na wierszu, a pracujące na całej tablicy pracują na całej tablicy. W drugim przykładzie metody pracują na swoim poziomie drzewa XML.
  • Łatwe testowanie - jednostki kodu podlegające testowaniu są małe, a dzięki temu łatwe do testowania.
  • Łatwe wyszukiwanie powtórzeń - w ramach jednej klasy można bardzo łatwo odnaleźć powtarzający się kod. W przypadku kilku klas też o ile użyjemy odpowiednich narzędzi szukających powtórzeń. Dzięki temu możemy usunąć powtórzenia i tym samym uzyskać jeszcze mniejszy i lepszy kod.
  • Łatwe wyszukiwanie drobnych błędów - w czasie refaktoryzacji możemy odnaleźć kod zbędny, wykonujący niepotrzebne operacje czy w końcu tworzony metodą kopiuj wklej.
  • Pomaga przestrzegać SRP - jeżeli powstaje dużo drobnych metod to możemy z łatwością stwierdzić, że klasa ma wiele odpowiedzialności i przeciwdziałać już na samym początku.
Wady
  • Duża ilość metod - w krótkim czasie otrzymujemy dużo małych metod. Trzeba umieć korzystać z mechanizmów IDE by swobodnie pomiędzy nimi wędrować. Rozwiązanie - część metod można przenieść do nowych klas pomocniczych, gdzie będą testowane i utrzymywane niezależnie od obecnego kontekstu użycia w kodzie.
  • Metody o podobnych nazwach - w czasie ekstrakcji metody otrzymują podobne nazwy. Rozwiązanie - jeżeli nazwy dobrze opisują metodę to dobrze. Jeżeli trudno dobrać taka nazwę to może oznaczać, że klasa ma za wiele odpowiedzialności.
Wrażenia i zalecenia

Użycie tej zasady jest proste i dzięki temu można ją stosować na najniższym poziomie. W praktyce tak samo jak puszczenie testów co chwilę tak i tu warto na bieżąco dbać o zachowanie tej zasady. Pozwoli to na szybką lokalizację błędów oraz naruszeń SRP. Wymaga to jednak dobrego opanowania IDE ponieważ dość szybko zamiast Extract Method zaczniemy używać Extract Class, a to już nie jest takie proste. No i najważniejsze. Drobne małe metody są wygodne jeżeli chcemy lokalizować błędy.

15 myśli na temat “Ekstremalna obiektowość w praktyce – część 1 – tylko jeden poziom zagłębienia na metodę

  1. W przykladzie z tablicami, można poprawić czytelność przekazując do metod wypełniających to co sugorowałyby ich nazwy, czyli odpowiednio tablice 3d, 2d i row 1d.

    private static int fill3dArray(int[][][] array3d) {
    int current = 0;
    for (int i = 0; i < array3d.length; i++) {
    current = fill2dArray(array3d[i], current);
    }
    return current;
    }

    private static int fill2dArray(int[][] array2d, int current) {
    for (int j = 0; j < array2d.length; j++) {
    current = fillRow(array2d[j], current);
    }
    return current;
    }

    private static int fillRow(int[] row, int current) {
    for (int k = 0; k < row.length; k++, current++) {
    row[k] = current;
    }
    return current;
    }

  2. Przykład na tablicach bardzo, ale to bardzo do mnie nie przemawia. Z 27 linii robi się 55. Te 27 da się ogarnąć wzrokiem i od razu powiedzieć, co tam się dzieje. Przykład „poprawiony” już trzeba studiować.

    Całe szczęście Wujek Bob żyje, bo przewracałby się w grobie. Nagle wykwitły nam metody, które przyjmują 4 argumenty, modyfikują jeden z nich, a do tego zwracają wartość. Jakby tego było mało, ułożone tak, żeby trudniej się czytało (najpierw pierwsza, potem trzecia, potem druga w łańcuszku).

    Pytanie: Jaki problem próbujesz rozwiązać? Bardziej wygląda na bardzo usilne naciąganie dla sztuki, bo ani czytelności, ani zrozumienia, ani łatwości utrzymania to nie podnosi. Wolałbym nie mieć z tym do czynienia w projekcie, gdzie linie kodu liczy się w setkach tysięcy.

  3. Konrad, pierwszy przykład jest trywialny, w drugim zmiany są już przydatne. Taki kod się znacznie łatwiej czyta i utrzymuje (o ile metody są w sensownej kolejności, tutaj tego nie ma). Jest to kod samo-dokumentujący się. Wujek Bob też tak robi 🙂

  4. W pierwszym przykładzie chodzi o ogólne przedstawienie zasady. Jeżeli chcielibyśmy ulepszyć ten kod zgodnie z zasadami Jeff’a Bay’a to należało by:
    – pojedynczą komórkę przedstawić nie jako int, ale przygotować dla niej osobną, znaczącą coś klasę (zasada nr 3)
    – każdy wiersz, tabelę dwu i w końcu główną tabelę trójwymiarową należało by opakować s klasę osłaniająca kolekcję i udostępniająca tylko potrzebne operacje biznesowe.
    – Należało by przygotować odpowiednie klasy zajmujące się wypełnianiem poszczególnych komórek, rzędów, tablic 2d i 3d.
    – Należało by przygotować odpowiednie klasy zajmujące się prezentowaniem poszczególnych…

    Nad tym przykładem będę się skupiał w kolejnych częściach właśnie dlatego, że banalnie prosty od strony biznesowej dobrze obrazuje zalety oraz wady takiego ekstremalnego podejścia.

  5. Koziołek – no właśnie, to tym bardziej przekonuje mnie, że z ekstremizmem trzeba ostrożnie. Po zastosowaniu wszystkich wskazówek zrobi się 12 klas i 300 linii kodu. Czasem chyba lepiej jest napisać jedną metodę/funkcję na 20 linii, która w bardzo spójny sposób robi to co jest do zrobienia, i łatwo da się zrozumieć i zweryfikować.

  6. @Konrad, to zależy jak na to patrzysz. Jeżeli przyjmiemy, że „wszystkie klasy nasze są” to rzeczywiście zaczyna się problem z ogarnięciem projektu. Biegunka klas jest jak każda inna biegunka – kłopotliwa. Z drugiej strony warto wydzielić jakieś fragmenty do osobnego projektu, który będzie udostępniał tylko API.

    Problem polega chyba jednak na czymś innym. Zobacz, że jak myślimy do metodykach Agile to mówimy o zespole. Zespół to od razu kilku ludzi, którzy współpracują i mają przydzielane zadania. Przydzielane są też role. Im mniejszy zespół tym agile gorzej działa. Zaczyna się przydzielanie różnych, często sprzecznych ról, do jednej osoby. Łamiemy swoiste zespołowe SRP. W takim kontekście jeżeli samodzielnie będziesz utrzymywał setki klas to szybko padniesz o ile nie potrafisz dobrze rozgraniczać różnych „kontekstów”. W praktyce trzeba wpędzić się w schizofrenię w ramach projektu by to działało.

  7. Nie w tym rzecz. Wydzielenie funkcjonalności, skrycie tego za API itd. to SOLID. Na tym etapie nic jeszcze nie narzuca ci aż takiego poszatkowania. Ba, nawet SOLID i clean code można zarzucać nadmierne rozdrobnienie, które utrudnia utrzymanie, weryfikację i zrozumienie.

    Wydzielenie tego do osobnego projektu i schowanie za jarem może ratować użytkownika (albo „projekt-matkę”). Tyle tylko, że to zamiatanie śmiecia pod dywan. Jak przyjdzie ci otworzyć taki projekt po pół roku i coś tam dodać albo zmienić, złapiesz się za głowę, osiwiejesz i spędzisz mnóstwo czasu próbując się rozeznać gdzie co było, a na końcu i tak nie będziesz pewien, czy twoja zmiana jest poprawna i bezpieczna.

    Drugi przykład wygląda już dużo lepiej, ale nie przez „jeden poziom zagłębienia”, tylko przez pójście w stronę clean code.

  8. Generalnie ta zasada prowadzi do Clean Code, czyli kodu w którym nie ma zbędnych elementów na danym poziomie abstrakcji. Tyle tylko, że jak już pisałem kolejne podziały kodu mogą doprowadzić do sytuacji gdzie trzeba będzie wydzielać klasy pomocnicze i dalej rozdrabniać kod. Utrzymanie czegoś takiego jest proste o ile jest dokumentacja i testy. Inaczej leżysz.

  9. Sztywne trzymanie sie opisanej metody moze przyniesc wiecej nieporozumien niz korzysci. Nie krytykuje pomyslu, ale nasuwaja sie kwestie, ktore trzeba by przemyslec.
    Przede wszystkim, jesli w klasie znajduje sie kilka metod z zagniezdzonymi blokami kodu, mozna skonczyc z calym sznurkiem pomocniczych metod, do ktorych nalezalo by napisac dokumentacje (bo kto bedzie pamietal, po co metoda x() zwraca jeden ze swoich argumentow – czyli np. jedna ze zmiennych sterujacych petli nadrzednej).
    Idac dalej. Co jesli blok wewnetrzny moze wygenerowac checked exception? Rodzi sie pytanie: jak/gdzie go obsluzyc bez naruszania program flow oraz jaki stan beda posiadac konkretne zmienne. I tu znow trzeba udokumientowac decyzje, aby za jakis czas nie odbily sie czkawka.
    I bardziej praktyczne pytanie to, jak taka refaktoryzacja wplynie na wydajnosc systemu.

  10. @Jacek, dużo małych metod pomocniczych pozwoli nam na wyłączenie ich do osobnej klasy. Zwiększy to elastyczność kodu i da możliwość jego wielokrotnego użycia. Jeżeli jakaś instrukcja może wywalić CE to i tak musimy to jakoś ogarnąć. Pytanie oczywiście czy sprzątamy na niższym czy na wyższym poziomie. Moim zdaniem nie będzie różnicy o ile oczywiście zachowamy pewną konwencję. Co więcej jeżeli nasz „niski poziom” będziemy przesuwać do osobnej klasy to rozwiązanie nasuwa się samo – metoda niskopoziomowa sypie wyjątkiem, a obsługa następuje na wysokim poziomie.
    Co do wydajności to jest to ciekawe zagadnienie. Jednak jak głoszą zasady optymalizacji – nie optymalizuj. Kod o krytycznym znaczeniu i tak będzie najprawdopodobniej napisać w zupełnie inny sposób. Stanowi on jednak niewielką część całości.

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