Jedna z zasad ekstremalnej obiektowości głosi, że klasy nie powinny mieć getterów i setterów. Jest to oczywiste, jeżeli weźmiemy pod uwagę zasadę hermetyzacjiW, która mówi, że powinniśmy nie ujawniać informacji zawartych w klasie. A czymże jest getter jak nie ujawnieniem takiej informacji, w dodatku czym różni się od publicznego pola?

W naszej przygodzie z lottomatem doszliśmy do momentu, że trzeba jakoś pokazać wyniki. Rzecz w tym, że ani klasa Ball, ani (Totolotek)ResultOfDraw nie mają metod, które pozwalają nam na wypisanie ich stanu. Najprościej było, by zaimplementować metodę toString i mieć z głowy. Jest to wystarczające rozwiązanie dla prostego przypadku, ale nie jest elastyczne na tyle, by można je uznać za dobre.

Metoda toString była, by fajna, gdyby można było do niej przekazać format, w jakim dane mają zostać wyświetlone albo był, by „jeden słuszny” format wyświetlania danych. Oba te podejścia mają w praktyce same wady. Pierwsze wymusza na wywołującym znajomość struktury wewnętrznej w tym identyfikatorów pól, by podać prawidłowy format. Drugie w praktyce mamy obecnie przy założeniu, że w ramach projektu toString implementujemy zawsze tym samym generatorem. Zmiana sposobu prezentacji informacji wymaga wyciągnięcia ich ze Stringa i sformatowania w nowy sposób. Jedynym ratunkiem jest wtedy założenie, że toString zwraca np. poprawnego JSONa i oparcie swojego kodu na takim założeniu.

Wzorcem go!

Skoro zatem nie chcemy korzystać z metody w rodzaju toString to sięgnijmy po odwrotne rozwiązanie. Niech nasza klasa ma metodę, która jako parametr przyjmie jakiś obiekt, który spełnia pewien kontrakt i przekazuje mu informacje o sobie. Mówiąc prościej, wzorzec wizytatora się kłania. Tradycyjna implementacja tego wzorca, taka jak na wikipediiW ma w sobie pewne wewnętrzne blech. Związane jest to ze sposobem implementacji w rodzaju:

Listing 1. Przykładowy wizytator

interface ExampleVisitor{
   void visitA(A a);
   void visitB(B b);
}

Pytanie, co będzie, jeżeli będziemy chcieli dodać kolejny obiekt do odwiedzenia? Czy powinniśmy dodać kolejny interfejs i zaimplementować go w odpowiednich klasach? Może należy dodać metodę visitC i zdać się na kompilator, który wyłapie braki w implementacji lub zrobić ją default? Jak zrobimy ją default to jakie powinno być to domyślne zachowanie? Czy metoda powinna „robić nic”, czy też rzucać UnsupportedOperationException? Jest to zajebiste pytanie na rozmowę kwalifikacyjną – co jeżeli chcemy dodać do wizytatora kolejny element?

Osobiście mam też inny problem z tym wzorcem. Czy nie łamie on SRP? Z jednej strony nasza klasa musi obsłużyć wizytatora, a z drugiej sam wizytator zazwyczaj ma kilka metod, które łączy jedynie pewna abstrakcja wykonywanego zadania, lecz nie mają one ze sobą nic wspólnego. Z drugiej strony mam takie jakieś dziwne przeczucie, że problem z tym wzorcem polega na przekuciu jeden do jednego diagramu UML. Pozostawmy te rozważania na bliżej nieokreśloną przyszłość. Spróbujmy zaimplementować rozwiązanie, które nie będzie łamać SRP.

Klasa wewnętrzna

Zdefiniujmy nasz problem. Potrzebujemy klasy, która ma dostęp do wewnętrznej reprezentacji Ball, na tej klasie będziemy dziś pracować (Totolotek)ResultOfDraw będzie zawierać analogiczne rozwiązanie, a jej zadaniem będzie ustalenie API, które da dostęp do tych informacji klasom z zewnątrz.

Pierwszy założenie spełnia tylko klasa, która jest klasą wewnętrzną i niestatyczną. Ponieważ ma ona dostęp do pól klasy, w której została zdefiniowana. API tej klasy może być w naszym przypadku bardzo proste i opierać się o bezpośrednie przekazanie wartości z obiektu. Innym rozwiązaniem, jest wprowadzenie deskryptora, który zawierałby informacje o np. nazwie pola. To możemy jednak uzyskać, tworząc kolejną klasę.

Mała uwaga. Java ma bardzo nieprzyjazny polimorfizm ad hoc. Jego zastosowanie powoduje, że klasa, w której jest używany, będzie puchła. Znacznie lepszym rozwiązaniem tego problemu są implicit conversions w Scali czy też rozszerzenia w Kotlinie. W przypadku tamtych rozwiązań kod rozszerzający nie znajduje się w samej klasie, a jedynie w pliku (jeśli dobrze pamiętam, by mieć dostęp do prywatnych elementów klasy, ale nie jestem pewien), przez co można go traktować jako swoisty plugin. W Javie namiastką tego rozwiązania są klasy wewnętrzne, ponieważ nie zmieniają API oryginalnej klasy. Jednakże muszą znajdować się w oryginalnej klasie, zatem naprawdę bardzo silnie wiążą się z kodem tej klasy.

Tradycyjnie zacznijmy od napisania testu:

Listing 2. Test klasy RawBallView

public class RawBallViewTest {

	@Test
	public void shouldAcceptPassBallValueToConsumer() throws Exception {
		Ball ball = new Ball(1);
		RawBallView sut = ball.new RawBallView();

		sut.accept(i -> Assertions.assertThat(i).isEqualTo(1));
	}
}

Odpalamy i… po zaspokojeniu kompilatora w temacie nazw, test będzie zielony… Metoda accept nie ma jeszcze bebechów, a test już zielony. Ot taka ciekawostka ze świata testów jednostkowych… Nasza asercja nie została nawet wywołana, a zatem nie miało się co wywalić. Poprawmy test:

Listing 3. Poprawiony test klasy RawBallView

public class RawBallViewTest {

	@Test
	public void shouldAcceptPassBallValueToConsumer() throws Exception {
		Ball ball = new Ball(1);
		RawBallView sut = ball.new RawBallView();

		Consumer<Integer> consumer = mock(Consumer.class);
		doAnswer(invocation -> Assertions.assertThat(invocation.getArguments()[0]).isEqualTo(1))
				.when(consumer).accept(1);

		sut.accept(consumer);
		verify(consumer, atLeastOnce()).accept(1);

	}
}

Odpalamy, będzie czerwono, bo klasa na razie nic nie zawiera ponad to co wymagane przez kompilator. Zatem jest OK.

Listing 4. Implementacja klasy RawBallView

public class Ball {

	private final Integer value;

	//... kod pominięty

	public class RawBallView{

		public void accept(Consumer<Integer> consumer){
        		consumer.accept(Ball.this.value);
		}
	}
}

I gotowe. To samo zrobimy dla ResultOdDraw i TotolotekResultOdDraw i już mamy elegancko zorganizowany kod. Jeżeli teraz będziemy chcieli dodać kolejny nośnik wiedzy np. w postaci klasy ExtendedRawBallView to nie musimy zmieniać żadnych innych klas. Po prostu w klasie Ball dodajemy kolejną klasę wewnętrzną i gotowe.

Podsumowanie

Powyższy kod jest nietypowym podejściem do problemu. Dzięki temu zachowaliśmy SRP. Spełniamy też założenie o nieujawnianiu zawartości klasy Ball na zewnątrz. Kod jest prosty do przetestowania. Nie narzuca, poza bardzo prostym API, ograniczeń co do sposobu pracy z wartościami. Nie zmusza też do tworzenia dużej ilości nadmiarowego kodu.

Kod jest dostępny w repozytorium na githubie.