Programistka ninja
  • Home
  • O nas
  • lista Blogów IT
Nowości
Switch over Object Type in Java
#personalnie: Java – Książki, które podobno musisz przeczytać
#whatTheHeck : Czym jest JavaEE i co ma...
Architektura Reaktywna
  • Home
  • O nas
  • lista Blogów IT
Programistka ninja Programistka ninja
Monthly Archives

październik 2019

    #toolboxProgramisty

    Switch over Object Type in Java

    by Paweł Widera 26 października 2019
    written by Paweł Widera

    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:

    1. Testy
    2. Implementacja
    3. Podsumowanie

    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 consumer) {
            onLog = Optional.of(consumer);
            return this;
        }
    
        public TypeSwitchBuilder onError(final Consumer consumer){
            this.onError = Optional.of(consumer);
            return this;
        }
    
        public TypeSwitchBuilder onMismath(final Consumer< Object> consumer) {
            this.onMismath = Optional.of(consumer);
            return this;
        }
    
        public TypeSwitch build() {
            return this;
        }
    
        public void handle(Object o) {
            List collect = matchersList.stream().filter(match -> match.aClass.equals(o.getClass())).collect(Collectors.toList());
            if(collect.size()==0) {
                onMismath.ifPresent( mis -> mis.accept(o));
            } else {
                try {
                    collect.forEach(match -> {
                        long start = System.currentTimeMillis();
                        match.aConsumer.accept(o);
                        onLog.ifPresent(log -> log.accept(o, System.currentTimeMillis()-start));
                    });
                } catch (Exception e) {
                    onError.ifPresent( err -> err.accept(e));
                }
            }
        }
    }
    
    interface TypeSwitch {
        void handle(Object o);
    }
    

    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 repo

    Podsumowanie

    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

    26 października 2019 2 komentarze
    3 FacebookTwitterLinkedinEmail

Najnowsze komentarze

  • Paweł Widera - Switch over Object Type in Java
  • Tomek - Switch over Object Type in Java
  • k4mil_m - #whatTheHeck : Czym jest JavaEE i co ma wspólnego ze Springiem?
  • ARR - #whatTheHeck : Czym jest JavaEE i co ma wspólnego ze Springiem?
  • A. - #whatTheHeck : Czym jest JavaEE i co ma wspólnego ze Springiem?

Archiwa

  • grudzień 2019
  • październik 2019
  • kwiecień 2018
  • kwiecień 2016

Kategorie

  • #personalnie
  • #reactiveArchitecture
  • #toolboxProgramisty
  • #whatTheHeck

Social Media

Facebook Twitter Instagram Email

Recent Posts

  • Architektura Reaktywna

    4 grudnia 2019
  • Switch over Object Type in Java

    26 października 2019
  • #personalnie: Java – Książki, które podobno musisz przeczytać

    19 kwietnia 2018
  • #whatTheHeck : Czym jest JavaEE i co ma wspólnego ze Springiem?

    14 kwietnia 2016

Dołącz do Newslettera

Bądz na bieżąco, Sięgnij po darmową wiedzę.

Ostatnie wpisy

  • Architektura Reaktywna
  • Switch over Object Type in Java
  • #personalnie: Java – Książki, które podobno musisz przeczytać
  • #whatTheHeck : Czym jest JavaEE i co ma wspólnego ze Springiem?

Dołącz do społeczności – zapisz się do newslettera!

Bądz na bieżąco, Sięgnij po darmową wiedzę.

  • Facebook
  • Twitter
  • Instagram
  • Email
  • Blogi programistyczne
  • Notatnik developerski
  • O nas

@2019 - All Right Reserved. Programistka.ninja

Programistka ninja
  • Home
  • O nas
  • lista Blogów IT
@2019 - All Right Reserved. Programistka.ninja