Ekstremalna obiektowość w praktyce – część 8 – opakowywanie kolekcji w klasy specyficzne dla kontekstu wykorzystania

Część 0
Część 1
Część 2
Część 3
Część 4
Część 5
Część 6
Część 7

Gladius Noctis, a na blogu ósma z zasad Jeff’a Bay’a

Klasa której polem jest kolekcja nie powinna mieć żadnych innych pól (opakowywanie kolekcji w klasy specyficzne dla kontekstu wykorzystania)

Nazwa trochę długa, ale nie ważne. Powróćmy na chwilę do pierwszej części tego cyklu. Na koniec przykładu z tablicami mieliśmy tam następujący kod:

Listing 1. Kod z pierwszej części

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 < array3d[i].length; j++) {
			current = fillRow(array3d, current, i, j);
		}
		return current;
	}

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

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

	private static void print2dArray(int[][][] array3d, int i) {
		for (int j = 0; j < array3d[i].length; j++) {
			printRow(array3d, i, j);
			System.out.println();
		}
	}

	private static void printArray(int[][][] array3d) {
		for (int i = 0; i < array3d.length; i++) {
			print2dArray(array3d, i);
			System.out.println();
		}
	}

	private static void printRow(int[][][] array3d, int i, int j) {
		for (int k = 0; k < array3d[i][j].length; k++) {
			System.out.print(String.format("%2s ", array3d[i][j][k]));
		}
	}

}

Przy tamtych założeniach było ok. Krawetko podał jeszcze zgrabniejszą wersję. Ja jednak zmienię tu wszystko.

Co robimy z kolekcjami?

Przede wszystkim iterujemy. Przeciskamy przez różne formy pętli w celu wykonania operacji na wszystkich elementach kolekcji. W Javie jest to dość upierdliwe. Za każdym razem należy napisać te klika linijek kodu by przepuścić daną kolekcję przez pętlę. W dodatku może okazać się, że coś skrewiliśmy albo nasz kod dostał się w niepowołane ręce, które wykonały kilka niefajnych operacji bezpośrednio na kolekcji.
W Scali jest trochę lepiej mamy metodę foreach, która opakowuje pętlę:

Listing 2. foreach w Scali

val x = 1 to 10
x foreach ($ => println($*$)) 

I już mamy kwadraty liczb od 1 do 10. W javie trzeba na to dwóch pętli.

Oczywiście jest tu ukryty mały „fakap” ponieważ zarówno metoda to jak i foreach maskują nam odpowiednie pętle. W przypadku to jest to jeszcze wzbogacone o lazy-init, ale to insza inszość.

Dlaczego opakowywać kolekcje?

Kod w Scali fajny jest. Dlatego warto stworzyć sobie podobne możliwości. W Javie nie jest to trudne generalnie poniższy kawałek kodu rzucił mi się kiedyś w oczy, ale skąd pochodzi ta koncepcja nie jestem wstanie powiedzieć.

Listing 3. foreach w Javie

interface Foreach<T> {

	interface Action<T> {
		void perform(T t);
	}

	abstract class ForeachImpl<T> implements Foreach<T> {

		private static class ArrayDelegate<T> implements Foreach<T> {

			private T[] array;

			public ArrayDelegate(T[] array) {
				this.array = array;
			}

			@Override
			public void foreach(Action<T> action) {
				for (T t : array) {
					action.perform(t);
				}
			}
		}

		private static class Delegate<T> extends ForeachImpl<T> {

			public Delegate(Collection<T> collection) {
				super(collection);
			}
		}

		public static <D> Foreach<D> delegate(Collection<D> collection) {
			return new Delegate<D>(collection);
		}

		public static <D> Foreach<D> delegate(D[] collection) {
			return new ArrayDelegate<D>(collection);
		}

		private Collection<T> collection;

		private ForeachImpl(Collection<T> collection) {
			this.collection = collection;
		}

		@Override
		public void foreach(Action<T> action) {
			for (T t : collection) {
				action.perform(t);
			}
		}
	}

	public void foreach(Action<T> action);
}

Jest to trochę karkołomna konstrukcja w tej postaci, ale wiadomo o co chodzi. Odpowiednie zagłębienia służą tu odwaleniu roboty podobnej do tej jaką odwala scalac przy dodawaniu traita do klasy. Dla przypomnienia jeżeli trait nie zawiera implementacji to jest traktowany jak interfejs. Jeżeli zawiera implementacje to tworzone są klasy abstrakcyjne zawierające zaimplementowane metody i w klasie docelowej następuje delegacja zachowania do takiej wewnętrznej klasy. Tyle w skrócie zainteresowanych odsyłam do konsoli i polecenia javap.

Nasza klasy wystarczy, że zaimplementuje interfejs i wydeleguje obsługę metody do klasy Delegate. Już dużo mniej roboty 😀 później przy iterowaniu przez kolekcję wystarczy zaimplementować clou pętli czyli to co pomiędzy { i }. Można to też łatwo przetestować. Ba! Można implementację przerzucić na użytkownika API. Niech powie co robi z pojedynczym elementem kolekcji, a to jak będzie przechodził przez kolekcję to już nie jego sprawa. Tu dochodzimy do takiego modelu, który jest już blisko programowania funkcyjnego. Klient mówi CO i nie martwi się JAK.

Wypełniamy tablicę

Powróćmy do pierwszego kodu. W pierwszym kroku opakujmy tablicę jednowymiarową o zadanej ilości elementów w coś przyjaznego dla środowiska:

Listing 4. Opakowana tablica intów

class ArrayOfInt {

	private Integer array[];

	public ArrayOfInt(int size) {
		array = new Integer[size];
	}

	public int fill(final int start) {
		return fill(start, 1);
	}

	public int fill(final int start, final int step) {
		int next = start;
		for (int i = 0; i () {

			@Override
			public void perform(Integer t) {
				System.out.print(String.format("%2s ", t));

			}
		});
	}

}

Ok, na razie nic groźnego. O ile inicjację tablicy i jej wypełnienie musimy przeprowadzić iterując z palucha to już wypisanie robimy za pomocą delegowania.
Teraz czas na tablicę dwu wymiarową. W tym przypadku musimy powiedzieć ile kolumn i ile rzędów chcemy mieć. Jednak czy aby na pewno? Opakowujemy w końcu kolekcję kolekcji… zatem…

Listing 5. Opakowana dwu wymiarowa tablica intów

class Array2dInt {

	private class Next {
		int next;
	}

	private ArrayOfInt array[];

	public Array2dInt(int rows, int cols) {
		array = new ArrayOfInt[rows];
		for (int i = 0; i < array.length; i++)
			array[i] = new ArrayOfInt(cols);
	}

	public int fill(final int start) {
		return fill(start, 1);
	}

	public int fill(final int start, final int step) {
		final Next next = new Next();
		next.next = start;
		ForeachImpl.delegate(array).foreach(new Action<ArrayOfInt>() {

			@Override
			public void perform(ArrayOfInt t) {
				next.next = t.fill(next.next, step);
			}
		});

		return next.next;
	}

	public void print() {
		ForeachImpl.delegate(array).foreach(new Action<ArrayOfInt>() {

			@Override
			public void perform(ArrayOfInt t) {
				t.print();
				System.out.println("");
			}
		});
	}
}

Whoa… inicjacja w konstruktorze nadal z palucha (tego nie przeskoczymy). Reszta przez delegaty. Tablica trójwymiarowa to już formalność.

Listing 6. Opakowana trójwymiarowa tablica intów

class Array3dInt {

	private class Next {
		int next;
	}

	private Array2dInt array[];

	public Array3dInt(int rows, int cols, int depth) {
		array = new Array2dInt[depth];
		for (int i = 0; i < array.length; i++)
			array[i] = new Array2dInt(rows, cols);
	}

	public int fill(final int start) {
		return fill(start, 1);
	}

	public int fill(final int start, final int step) {
		final Next next = new Next();
		next.next = start;
		ForeachImpl.delegate(array).foreach(new Action<Array2dInt>() {

			@Override
			public void perform(Array2dInt t) {
				next.next = t.fill(next.next, step);
			}
		});

		return next.next;
	}

	public void print() {
		ForeachImpl.delegate(array).foreach(new Action<Array2dInt>() {

			@Override
			public void perform(Array2dInt t) {
				t.print();
				System.out.println("");
			}
		});
	}
}

Całość można oczywiście ładnie rozdzielić na nieanonimowe klasy wewnętrzne (albo wyrzucić do osobnych klas o ograniczonej widoczności) czy uczynić generycznym. Cały kod jest, wbrew pozorom, prostszy i łatwiejszy w testowaniu.

Podsumowanie

Bardzo istotną rzeczą w takim podejściu do kodowania jest umiejętność rozdzielenia swojej osobowości na klienta i dostawcę. W przeciwnym wypadku możemy nieźle przestraszyć się ilością kodu.

Najistotniejszą cechą jest jednak ograniczenie klientowi pola manewru w pracy z kolekcją. Mając ograniczone API i pozbawiając go możliwości dowolnego manipulowania elementami możemy się zabezpieczyć przed nieprzyjemnymi sytuacjami jak chociażby usunięcie elementu z kolekcji (boli jak używasz JPA), głębokie klonowanie kolekcji albo nieskończonego iterowania.
Dodatkowo operacje biznesowe są jasno określone przez co zmniejsza się prawdopodobieństwo „hakierowania” i tworzenia obejść we właściwym kodzie biznesowym.

Gdzie warto używać

Przede wszystkim warto opakowywać kolekcje, które wychodzą z bazy danych/DAO. W ten sposób można ograniczyć manipulowanie obiektami w ramach kodu biznesowego. Wszelkie operacje związane ze zmianą stanu bazy będą dokonywane poprzez interfejs opakowanej kolekcji. Dzięki czemu można kontrolować kto co może. W takim układzie aplikacja nie widzi DAO jako takiego. Widzi tylko interfejs dostarczający pewne dane na których można wykonać ściśle określone operacje.
Po drugie MVP. W tym przypadku kolekcja staje się pomocnikiem dla prezentera. Można dzięki temu wprowadzić np. dodatkowe filtrowanie danych tuż przed ich wyświetleniem. To pozwala na stworzenie jednego prezentera, który będzie prezentował dane w zależności od potrzeb, ale będzie jednocześnie na tyle elastyczny by móc z niego skorzystać w wielu miejscach. Przykładowo kolekcja A przed wyświetleniem wymaga wycięcia części danych, których nie bardzo można pozbyć się na etapie pobierania z bazy. Jednocześnie kolekcja B nie ma tych danych już domyślnie. Obie mają metodę show przyjmującą jako parametr prezenter. Następnie w środku wywołują tylko wybrane metody prezentera.
Po trzecie filtry. W takim przypadku zamiast pozwolić iterować po kolekcji możemy udostępnić klientowi ileś tam predefiniowanych metod, które filtrują kolekcję w określony sposób. Tu mała uwaga. Szkoda, że w Javie nie ma eleganckich domknięć i trzeba stosować haki w stylu anonimowych klas.

4 myśli na temat “Ekstremalna obiektowość w praktyce – część 8 – opakowywanie kolekcji w klasy specyficzne dla kontekstu wykorzystania

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