Zastanawiałeś się kiedyś czy da się użyć instrukcji switch podając do niej obiekt dowolnej klasy? Wraz z pojawieniem się 7 wersji javy programiści dostali możliwość wykonywania instrukcji switch podając jako argument obiekt typu String.
Sekcje w tym poście:
Przykładowy sposób użycia mógłby zatem wyglądać następująco…
package ninja.programista.typeswitch;
import org.junit.Assert;
import org.junit.Test;
public class StringSwitch {
private String getCountryCapitol(String country) {
String capitol;
switch (country) {
case "Poland" : capitol = "Warsaw"; break;
case "France" : capitol = "Paris"; break;
default: capitol = "unknown"; break;
}
return capitol;
}
@Test
public void stringTest() {
Assert.assertEquals(getCountryCapitol("Poland"), "Warsaw");
Assert.assertEquals(getCountryCapitol("France"), "Paris");
Assert.assertEquals(getCountryCapitol("Uganda"), "unknown");
}
}
Jak widać rozwiązanie jest bardzo szybkie i przyjemne. Co jednak jeśli chcielibyśmy wykorzystać tą instrukcje do sprawdzania czegoś więcej? np. typów samych obiektów? Byłoby to bardzo pomocne w aplikacjach, które posługują się np. eventami w komunikacji.
Załóżmy zatem, że mamy 3 klasy reprezentujące różne eventy. Dla uproszczenia zdeklarujemy je wszystkie w tej samej klasie.
package ninja.programista.typeswitch;
public class Communication {
static class AddCustomerEvent{}
static class RemoveCustomerEvent{}
static class ModifyCustomerEvent{}
}
Co więcej chcielibyśmy również mieć 3 metody onEvent() przyjmujące wybrany typ eventu do obsługi każdego z nich. Czyli zmodyfikujmy jeszcze naszą klasę następująco.
package ninja.programista.typeswitch;
public class Communication {
static class AddCustomerEvent{}
static class RemoveCustomerEvent{}
static class ModifyCustomerEvent{}
public void onEvent(AddCustomerEvent event) {
// do stuff
}
public void onEvent(ModifyCustomerEvent event) {
// do stuff
}
public void onEvent(RemoveCustomerEvent event) {
// do stuff
}
}
Wtedy w wybranym miejscu aplikacji w zależności od typu eventu wykonujemy inne instrukcje.
package ninja.programista.typeswitch;
import org.junit.Test;
public class TypeSwitchTest{
@Mock
private Communication c;
@Test
public void switchOverClassTest() {
processEvent(new AddCustomerEvent());
processEvent(new ModifyCustomerEvent());
processEvent(new RemoveCustomerEvent());
verify(c).onEvent(any(AddCustomerEvent.class));
verify(c).onEvent(any(ModifyCustomerEvent.class));
verify(c).onEvent(any(RemoveCustomerEvent.class));
}
private void processEvent(Object event) {
switch (event.getClass().getSimpleName()) {
case "AddCustomerEvent": c.onEvent((AddCustomerEvent)event);
break;
case "ModifyCustomerEvent": c.onEvent((ModifyCustomerEvent) event);
break;
case "RemoveCustomerEvent": c.onEvent((RemoveCustomerEvent)event);
break;
default:
// throw unknown event exception
}
}
}
Ostatecznie jednak chcielibyśmy uniknąć zbędnego rzutowania i pozwolić Javie zadbać o weryfikacje typów, zamiast konwertować je do Stringa. Przejdźmy zatem do naszego własnego rozwiązania.
Testy
Zacznijmy pisanie modyfikacji od testów zamiast od kodu. Pierwszy test jaki byśmy napisali, który zobrazowałby to co chcemy osiągnąć wyglądałby tak..
@Test
public void matchTest() {
myTypeSwitch = TypeSwitchBuilder.getInstance()
.with(AddCustomerEvent.class, c::onEvent)
.with(ModifyCustomerEvent.class, c::onEvent)
.with(RemoveCustomerEvent.class, c::onEvent)
.build();
myTypeSwitch.handle(new AddCustomerEvent());
myTypeSwitch.handle(new ModifyCustomerEvent());
myTypeSwitch.handle(new RemoveCustomerEvent());
verify(c).onEvent(any(AddCustomerEvent.class));
verify(c).onEvent(any(ModifyCustomerEvent.class));
verify(c).onEvent(any(RemoveCustomerEvent.class));
}
Idąc typ tropem, określmy kolejne zachowania, które nasz „mechanizm” powinien implementować. Na pewno fajnie byłoby rzucić jakiś wyjątek w przypadku pojawienia się nowego nieznanego eventu.
@Test(expected = IllegalArgumentException.class)
public void missingMatch() {
myTypeSwitch = TypeSwitchBuilder.getInstance()
.onMismath(this::throwException);
myTypeSwitch.handle(new String());
}
private void throwException(Object o) {
throw new IllegalArgumentException("missing match for event " + c.getClass());
}
Dodatkowo jeżeli w naszym kodzie obsługi eventu wystąpi jakiś błąd, to może chcielibyśmy go przechwycić i zrobić jakąś wspólna obsługę wyjątków np. zamienić je na typ bardziej spersonalizowany.
@Test(expected = CustomException.class)
public void onError() {
myTypeSwitch = TypeSwitchBuilder.getInstance()
.with(RemoveCustomerEvent.class, c::onEvent)
.onError(TypeSwitchTest::replaceWithCustomError);
Mockito.doThrow(new IllegalStateException()).when(c).onEvent(any(RemoveCustomerEvent.class));
// methoda rzuca IllegalStateException /|\, a my zamieniamy go na CustomException
myTypeSwitch.handle(new RemoveCustomerEvent());
}
private static void replaceWithCustomError(Exception e) {
throw new CustomException(e);
}
W niektórych przypadkach przydałoby się też jakieś wspólne logowanie.
@Test
public void withLog() {
myTypeSwitch = TypeSwitchBuilder.getInstance()
.with(AddCustomerEvent.class, c::onEvent)
.with(ModifyCustomerEvent.class, c::onEvent)
.with(RemoveCustomerEvent.class, c::onEvent)
.withPerfLog((o, duration) ->
logger.info(String.format("Processed event %s in time %d", o, duration)));
myTypeSwitch.handle(new AddCustomerEvent());
myTypeSwitch.handle(new ModifyCustomerEvent());
myTypeSwitch.handle(new RemoveCustomerEvent());
verify(logger).info(contains("Processed event ninja.programista.typeswitch.Communication$AddCustomerEvent"));
verify(logger).info(contains("Processed event ninja.programista.typeswitch.Communication$ModifyCustomerEvent"));
verify(logger).info(contains("Processed event ninja.programista.typeswitch.Communication$RemoveCustomerEvent"));
}
Implementacja
I tak po zdefiniowaniu wszystkich testów, możemy przystąpić do kodowania.
package ninja.programista.typeswitch;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.stream.Collectors;
public class TypeSwitchBuilder implements TypeSwitch {
private class TypeMatch {
private Class aClass;
private Consumer aConsumer;
public TypeMatch(Class aClass, Consumer aConsumer) {
this.aClass = aClass;
this.aConsumer = aConsumer;
}
}
// ten obiekt moze byc lista, setem lub mapa w zaleznosci od zachowania ktore chcemy uzyskac
private List matchersList = new ArrayList<>();
private Optional onMismath = Optional.empty();
private Optional> onError = Optional.empty();
private Optional> onLog = Optional.empty();
public static TypeSwitchBuilder getInstance() {
return new TypeSwitchBuilder();
}
public TypeSwitchBuilder with(final Class targetClass, final Consumer consumer) {
matchersList.add(new TypeMatch(targetClass, consumer));
return this;
}
public TypeSwitchBuilder withPerfLog(final BiConsumer
Klasa działa w oparciu o standardowy obiekt Consumer-a z javy 8. Mamy zatem listę consumerów matchersList, która przechowuje akcje w oparciu o konkretny typ eventu. W zależności od zachowania, które chcemy uzyskać może on być zamieniony na Map lub Set. Dodatkowo znajdziemy w klasie obiekty Consumer dla operacji onMismatch, onError i onLog. Całość została opakowania w interfejs TypeSwitch w celu zamknięcia dostępu do metod budujących. Zachęcam do ściągnięcia sobie repo, pobawienia się testami i modyfikowania mechanizmu wedle własnych potrzeb.
Kod źródłowy:
Kod źródłowy wraz z testami można znaleźć na githubie: TypeSwitch repoPodsumowanie
W dzisiejszych czasach takie pojęcia jak DDD, Event Sourcing czy event-driven systems szturmują nasze programistyczne zmysły, a eventy staja się podstawowym nośnikiem informacji. Posiadanie zatem klas pomocniczych, które uporządkują nasz kod jest na wagę złotą. Dobry kod to prosty kod, niestety taki najtrudniej napisać. Nic nie stoi zatem na przeszkodzie, żeby co bardziej zawirowane kawałki schować w klasach pomocniczych i skupić się na wystawieniu czytelnego i naturalnego api zarówno dla juniora jak i seniora. Remeber! KISS