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 
<p>Realizacja w <samp>main</samp> tylko dla wygody uruchamiania. Z tego powodu dalej będę używał metod statycznych.</p>
<p>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ęć.<br></br>
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.</p>
<p class="listing">Listing 2. Kod po pierwszej refaktoryzaci</p>java
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 
<p>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 <samp>main</samp> do osobnych metod:</p>
<p class="listing">Listing 3. Kod po drugiej refaktoryzaci</p>java
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 
<p>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 <samp>current</samp>? 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 <samp>main</samp> 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ą.<br></br>
W wyniku wszystkich tych kroków otrzymujemy poniższy kod.</p>
<p class="listing">Listing 4. Kod zakończeniu pracy</p>java
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 
<p>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. </p>
<h5>A może coś bardziej życiowego?</h5>
<p>Przykład był mało życiowy, ale prosty. Czas na coś bardziej życiowego.<br></br>
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.<br></br>
W uproszczeniu cały program wyglądał by mniej więcej tak</p>
<p class="listing">Listing 5. Kod mapowania XML - Obiekt</p>java
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 = "<transaction>" + "<id>1</id>"
				+ "<state>OK</state>" + "<senderid>100</senderid>"
				+ "<reciverid>101</reciverid>" + "</transaction>";
		String transaction2 = "<transaction>" + "<id>2</id>"
				+ "<state>OK</state>" + "<senderid>101</senderid>"
				+ "<reciverid>100</reciverid>" + "</transaction>";
		String response = "<transactions>" + transaction1 + transaction2
				+ "</transactions>";

		Unmarshaller unmarshaller = new Unmarshaller();
		Collection<transaction> 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<transaction> 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<transaction> transactions = new ArrayList<transaction>(length);
		for (int i = 0; i 
<p>Klasa <samp>Unmarshaller</samp> 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:</p>
<p class="listing">Listing 6. Kod po szybkiej refaktoryzacji</p>java
class Unmarshaller {

	Collection<transaction> 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<transaction> transactions = new ArrayList<transaction>(length);
		for (int i = 0; i 
<p>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.</p>
<p class="listing">Listing 7. Kod po dokładniejszej refaktoryzacji</p>java
class Unmarshaller {

	Collection<transaction> 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<transaction> transactions = new ArrayList<transaction>(length);
		for (int i = 0; i 
<p>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.</p>
<h4>Czas na małe podsumowanie</h4>
<p>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. </p>
<h5>Zalety</h5>
<ul><li><b>Prostota</b> - zasada prosta do zachowania. Nie wymaga dużo czasu i nakładu pracy.</li>
<li><b>Rozsądna granulacja kodu</b> - 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.</li>
<li><b>Łatwe testowanie</b> - jednostki kodu podlegające testowaniu są małe, a dzięki temu łatwe do testowania.</li>
<li><b>Łatwe wyszukiwanie powtórzeń</b> - 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.</li>
<li><b>Łatwe wyszukiwanie drobnych błędów</b> - w czasie refaktoryzacji możemy odnaleźć kod zbędny, wykonujący niepotrzebne operacje czy w końcu tworzony metodą kopiuj wklej.</li>
<li><b>Pomaga przestrzegać SRP</b> - 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.</li>
</ul><h5>Wady</h5>
<ul><li><b>Duża ilość metod</b> - w krótkim czasie otrzymujemy dużo małych metod. Trzeba umieć korzystać z mechanizmów IDE by swobodnie pomiędzy nimi wędrować. <i>Rozwiązanie</i> - część metod można przenieść do nowych klas pomocniczych, gdzie będą testowane i utrzymywane niezależnie od obecnego kontekstu użycia w kodzie.</li>
<li><b>Metody o podobnych nazwach</b> - w czasie ekstrakcji metody otrzymują podobne nazwy. <i>Rozwiązanie</i> - 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.</li>
</ul><h5>Wrażenia i zalecenia</h5>
<p>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ń <a href="https://koziolekweb.pl/2009/02/26/solidne-programowanie-czesc-1-czyli-monogamia/">SRP</a>. 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.</p></transaction></transaction></transaction></transaction></transaction></transaction></transaction></transaction></transaction></transaction>