Czy zgłaszanie wyjątku jest tu anty-wzorem?

33

Właśnie omówiłem wybór projektu po przejrzeniu kodu. Zastanawiam się, jakie są twoje opinie.

Jest ta Preferencesklasa, która jest wiadrem dla par klucz-wartość. Wartości zerowe są legalne (to ważne). Oczekujemy, że niektóre wartości mogą jeszcze nie zostać zapisane i chcemy automatycznie obsługiwać te przypadki, inicjując je z predefiniowaną wartością domyślną na żądanie.

W omawianym rozwiązaniu zastosowano następujący wzorzec (UWAGA: oczywiście nie jest to rzeczywisty kod - oczywiście jest uproszczony w celach ilustracyjnych):

public class Preferences {
    // null values are legal
    private Map<String, String> valuesFromDatabase;

    private static Map<String, String> defaultValues;

    class KeyNotFoundException extends Exception {
    }

    public String getByKey(String key) {
        try {
            return getValueByKey(key);
        } catch (KeyNotFoundException e) {
            String defaultValue = defaultValues.get(key);
            valuesFromDatabase.put(key, defaultvalue);
            return defaultValue;
        }
    }

    private String getValueByKey(String key) throws KeyNotFoundException {
        if (valuesFromDatabase.containsKey(key)) {
            return valuesFromDatabase.get(key);
        } else {
            throw new KeyNotFoundException();
        }
    }
}

Został skrytykowany jako anty-wzór - nadużywanie wyjątków w celu kontrolowania przepływu . KeyNotFoundException- wprowadzone w życie tylko dla tego jednego przypadku użycia - nigdy nie będzie postrzegane poza zakresem tej klasy.

Zasadniczo są to dwie metody zabawy z pobieraniem go, po prostu komunikowanie sobie czegoś.

Klucz nieobecny w bazie danych nie jest czymś niepokojącym ani wyjątkowym - spodziewamy się, że tak się stanie, ilekroć zostanie dodane nowe ustawienie preferencji, stąd mechanizm, który w razie potrzeby z wdziękiem inicjuje wartość domyślną.

Kontrargumentem było to, że getValueByKey- metoda prywatna - jak zdefiniowano w tej chwili, nie ma naturalnego sposobu informowania metody publicznej zarówno o wartości, jak io tym, czy klucz tam był. (Jeśli nie, należy go dodać, aby można było zaktualizować wartość).

Powrót nullbyłby niejednoznaczny, ponieważ nulljest to całkowicie legalna wartość, więc nie wiadomo, czy oznaczało to, że klucza nie było, czy też był null.

getValueByKeymusiałby zwrócić jakiś a Tuple<Boolean, String>, bool ustawiony na true, jeśli klucz już tam jest, abyśmy mogli odróżnić od (true, null)i (false, null). (W języku outC # można użyć parametru, ale to jest Java).

Czy to ładniejsza alternatywa? Tak, musiałbyś zdefiniować jakąś klasę jednorazowego użytku Tuple<Boolean, String>, ale wtedy się pozbywamy KeyNotFoundException, więc ten rodzaj równowagi się wyrówna. Unikamy również narzutu związanego z obsługą wyjątku, chociaż nie jest to istotne z praktycznego punktu widzenia - nie ma tu żadnych wzmianek o wydajności, jest to aplikacja kliencka i to nie tak, że preferencje użytkownika będą pobierane miliony razy na sekundę.

Odmiana tego podejścia może używać Guawy Optional<String>(Guawa jest już używana w całym projekcie) zamiast jakiegoś niestandardowego Tuple<Boolean, String>, a następnie możemy rozróżnić Optional.<String>absent()i „właściwe” null. Nadal jednak wydaje się hacking z powodów, które są łatwe do zauważenia - wprowadzenie dwóch poziomów „nieważności” wydaje się nadużywać koncepcji stojącej za tworzeniem Optionals na pierwszym miejscu.

Inną opcją byłoby jawne sprawdzenie, czy klucz istnieje (dodaj boolean containsKey(String key)metodę i wywołaj getValueByKeytylko, jeśli już stwierdziliśmy, że istnieje).

Na koniec można również wstawić metodę prywatną, ale rzeczywista getByKeyjest nieco bardziej złożona niż mój przykładowy kod, więc wstawianie mogłoby sprawić, że wyglądałaby dość paskudnie.

Być może dzielę włosy tutaj, ale jestem ciekawy, na co postawiłbyś się, aby być najbliższym najlepszej praktyki w tym przypadku. Nie znalazłem odpowiedzi w przewodnikach po stylach Oracle i Google.

Czy stosowanie wyjątków, takich jak w przykładowym kodzie, jest anty-wzorcem, czy jest to dopuszczalne, biorąc pod uwagę, że alternatywy również nie są bardzo czyste? Jeśli tak, to w jakich okolicznościach byłoby dobrze? I wzajemnie?

Konrad Morawski
źródło
1
@ GlenH7 zaakceptowana (i najwyżej głosowana) odpowiedź podsumowuje, że „powinieneś ich używać [wyjątki] w przypadkach, gdy ułatwiają obsługę błędów przy mniejszym bałaganie kodu”. Myślę, że pytam tutaj, czy wymienione przeze mnie alternatywy należy uznać za „mniej bałaganu w kodzie”.
Konrad Morawski
1
Cofnąłem swoje VTC - prosisz o coś związanego, ale innego niż duplikat, który zasugerowałem.
3
Myślę, że pytanie staje się bardziej interesujące, gdy getValueByKeyjest również publiczne.
Doc Brown
2
Na przyszłość zrobiłeś słusznie. To byłoby nie na temat recenzji kodu, ponieważ jest to przykładowy kod.
RubberDuck,
1
Wystarczy wejść w to --- jest to całkowicie idiomatyczny Python i (moim zdaniem) byłby preferowanym sposobem radzenia sobie z tym problemem w tym języku. Oczywiście różne języki mają różne konwencje.
Patrick Collins,

Odpowiedzi:

75

Tak, twój kolega ma rację: to zły kod. Jeśli błąd można rozwiązać lokalnie, należy go natychmiast naprawić. Wyjątek nie powinien być zgłaszany, a następnie obsługiwany natychmiast.

Jest to o wiele czystsze niż Twoja wersja ( getValueByKey()metoda została usunięta):

public String getByKey(String key) {
    if (valuesFromDatabase.containsKey(key)) {
        return valuesFromDatabase.get(key);
    } else {
        String defaultValue = defaultValues.get(key);
        valuesFromDatabase.put(key, defaultvalue);
        return defaultValue;
    }
}

Wyjątek należy zgłosić tylko wtedy, gdy nie wiesz, jak rozwiązać błąd lokalnie.

BЈовић
źródło
14
Dziękuję za odpowiedź. Dla przypomnienia, jestem tym kolegą (nie podobał mi się oryginalny kod), po prostu sformułowałem pytanie neutralnie, aby nie zostało załadowane :)
Konrad Morawski
59
Pierwszy znak szaleństwa: zaczynasz mówić do siebie.
Robert Harvey,
1
@ BЈовић: cóż, w takim przypadku myślę, że jest w porządku, ale co, jeśli potrzebujesz dwóch wariantów - jednej metody pobierania wartości bez autokreacji brakujących kluczy, a drugiej? Jak myślisz, jaka jest zatem najczystsza (i SUCHA) alternatywa?
Doc Brown
3
@RobertHarvey :) ktoś inny napisał ten fragment kodu, osobna osobna osoba, byłem recenzentem
Konrad Morawski
1
@Alvaro, używanie wyjątków często nie stanowi problemu. Niewłaściwe korzystanie z wyjątków stanowi problem, niezależnie od języka.
kdbanman
11

Nie nazwałbym tego zastosowania wyjątków anty-wzorcem, po prostu nie najlepszym rozwiązaniem problemu komunikowania złożonego wyniku.

Najlepszym rozwiązaniem (zakładając, że nadal korzystasz z Java 7) byłoby użycie Opcji Guava; Nie zgadzam się, że jego użycie w tym przypadku byłoby hackerskie. Wydaje mi się, w oparciu o rozszerzone wyjaśnienie Guava o Opcjonalnym , że jest to doskonały przykład tego, kiedy go użyć. Rozróżniasz między „nie znaleziono wartości” a „wartością null znaleziono”.

Mike Partridge
źródło
1
Nie sądzę, że Optionalmoże być zerowy. Optional.of()przyjmuje tylko odwołanie inne niż null i Optional.fromNullable()traktuje null jako „wartość nieobecna”.
Navin
1
@Navin nie może mieć wartości zerowej, ale masz Optional.absent()do dyspozycji. I tak Optional.fromNullable(string)będzie równy, Optional.<String>absent()jeśli stringbył zerowy, lub Optional.of(string)jeśli nie był.
Konrad Morawski
@KonradMorawski Myślałem, że problem w OP polegał na tym, że nie można było odróżnić łańcucha zerowego od łańcucha nieistniejącego bez zgłaszania wyjątku. Optional.absent()obejmuje jeden z tych scenariuszy. Jak reprezentujesz ten drugi?
Navin
@Navin z faktycznym null.
Konrad Morawski
4
@KonradMorawski: To brzmi jak naprawdę zły pomysł. Trudno mi wymyślić jaśniejszy sposób powiedzenia „ta metoda nigdy nie powraca null!” niż zadeklarować to jako powrót Optional<...>. . .
ruakh
8

Ponieważ nie ma żadnych rozważań dotyczących wydajności i jest to szczegół implementacji, ostatecznie nie ma znaczenia, które rozwiązanie wybierzesz. Ale muszę się zgodzić, że to zły styl; nieobecność klucza jest czymś, o czym wiesz, że się wydarzy, a nawet nie radzisz sobie z więcej niż jednym wywołaniem stosu, czyli tam, gdzie wyjątki są najbardziej przydatne.

Podejście krotkowe jest nieco zhackowane, ponieważ drugie pole nie ma znaczenia, gdy wartość logiczna jest fałszywa. Sprawdzanie, czy klucz istnieje wcześniej, jest głupie, ponieważ mapa szuka klucza dwa razy. (Cóż, już to robisz, więc w tym przypadku trzy razy.) OptionalRozwiązanie idealnie pasuje do problemu. Może się wydawać trochę ironiczne, aby przechowywać nullw Optional, ale jeśli to jest to, co użytkownik chce zrobić, nie możesz odmówić.

Jak zauważył Mike w komentarzach, jest z tym problem; ani Guava, ani Java 8 nie Optionalpozwalają na przechowywanie nulls. Dlatego musisz rzucić własny, który - choć prosty - wymaga sporej ilości płyty kotłowej, więc może to być przesada dla czegoś, co zostanie użyte tylko raz wewnętrznie. Możesz także zmienić typ mapy na Map<String, Optional<String>>, ale obsługa Optional<Optional<String>>s staje się niewygodna.

Rozsądnym kompromisem może być utrzymanie wyjątku, ale uznanie jego roli jako „alternatywnego zwrotu”. Utwórz nowy sprawdzony wyjątek z wyłączonym śledzeniem stosu i wrzuć jego wstępnie utworzoną instancję, którą możesz przechowywać w zmiennej statycznej. Jest to tanie - być może tak tanie jak lokalny oddział - i nie można zapomnieć o tym, aby je obsłużyć, więc brak śledzenia stosu nie stanowi problemu.

Doval
źródło
1
W rzeczywistości jest to tylko Map<String, String>na poziomie tabeli bazy danych, ponieważ valueskolumna musi być jednego typu. Istnieje jednak otulająca warstwa DAO, która silnie wpisuje każdą parę klucz-wartość. Ale pominąłem to dla jasności. (Oczywiście możesz mieć zamiast tego tabelę z jednym wierszem z kolumnami n, ale dodanie każdej nowej wartości wymaga zaktualizowania schematu tabeli, więc usunięcie złożoności związanej z koniecznością ich analizy jest kosztem wprowadzenia innej złożoności w innym miejscu).
Konrad Morawski
Dobra uwaga na temat krotki. Rzeczywiście, co miałoby (false, "foo")to znaczyć?
Konrad Morawski
Przynajmniej z Opcją Guawy, próba umieszczenia wartości null w wynikach jest wyjątkiem. Będziesz musiał użyć Optional.absent ().
Mike Partridge
@MikePartridge Dobry punkt. Brzydkim obejściem byłoby zmienić mapę na Map<String, Optional<String>>i wtedy byś to zrobił Optional<Optional<String>>. Mniej mylącym rozwiązaniem byłoby wyrzucenie własnego Opcjonalnego, który pozwala nulllub podobny typ danych algebraicznych, który nie zezwala, nullale ma 3 stany - nieobecny, zerowy lub obecny. Oba są łatwe do wdrożenia, chociaż ilość płyty kotłowej jest wysoka dla czegoś, co będzie używane tylko wewnętrznie.
Doval
2
„Ponieważ nie ma żadnych rozważań dotyczących wydajności i jest to szczegół implementacji, ostatecznie nie ma znaczenia, które rozwiązanie wybierzesz” - z wyjątkiem tego, że jeśli używasz C #, użycie wyjątku zirytuje każdego, kto próbuje debugować za pomocą „Przerwa, gdy wyjątek zostanie zgłoszony ”jest włączony dla wyjątków CLR.
Stephen
5

Jest ta klasa Preferencji, która jest wiadrem dla par klucz-wartość. Wartości zerowe są legalne (to ważne). Oczekujemy, że niektóre wartości mogą jeszcze nie zostać zapisane i chcemy automatycznie obsługiwać te przypadki, inicjując je z predefiniowaną wartością domyślną na żądanie.

Problem jest właśnie taki. Ale już opublikowałeś rozwiązanie:

Odmiana tego podejścia może wykorzystywać Opcjonalną Guawę (Guawa jest już używana w całym projekcie) zamiast jakiejś niestandardowej Tuple, a następnie możemy rozróżnić Optional.absent () od „właściwej” wartości null . Nadal jednak wydaje się hacking z powodów, które są łatwe do zauważenia - wprowadzenie dwóch poziomów „nieważności” wydaje się nadużywać koncepcji stojącej za tworzeniem Opcjonalnych.

Nie używaj jednak null lub Optional . Możesz i powinieneś używać Optionaltylko. Aby poczuć się „hackersko”, po prostu użyj go zagnieżdżonego, aby uzyskać Optional<Optional<String>>wyraźne stwierdzenie, że w bazie danych może być klucz (pierwsza warstwa opcji) i że może on zawierać predefiniowaną wartość (druga warstwa opcji).

Takie podejście jest lepsze niż stosowanie wyjątków i jest dość łatwe do zrozumienia, o ile Optionalnie jest dla Ciebie nowe.

Należy również pamiętać, że Optionalma kilka funkcji komfortu, dzięki czemu nie musisz wykonywać całej pracy sam. Obejmują one:

  • static static <T> Optional<T> fromNullable(T nullableReference)przekonwertować dane wejściowe bazy danych na Optionaltypy
  • abstract T or(T defaultValue) aby uzyskać wartość klucza wewnętrznej warstwy opcji lub (jeśli nie występuje) uzyskać domyślną wartość klucza
walenteria
źródło
1
Optional<Optional<String>>wygląda źle, choć ma pewien urok, muszę przyznać:)
Konrad Morawski
Czy możesz wyjaśnić dalej, dlaczego wygląda źle? Jest to właściwie ten sam wzór co List<List<String>>. Możesz oczywiście (jeśli pozwala na to język) utworzyć nowy typ, DBConfigKeyktóry otacza Optional<Optional<String>>i czyni go bardziej czytelnym, jeśli wolisz.
valenterry
2
@valenterry Jest bardzo inaczej. Lista list to uzasadniony sposób przechowywania niektórych rodzajów danych; Opcjonalne Opcjonalne to nietypowy sposób wskazywania dwóch rodzajów problemów, jakie mogą mieć dane.
Ypnypn
Opcja opcji jest jak lista list, na której każda lista zawiera jeden lub żaden element. Zasadniczo jest tak samo. To, że nie jest często używane w Javie , nie oznacza, że ​​jest z natury złe.
valenterry
1
@valenterry zło w tym sensie, że Jon Skeet oznacza; więc nie całkiem zły, ale trochę zły, „sprytny kod”. Wydaje mi się, że jest to nieco barokowe, opcjonalne opcjonalne. Nie jest jasne, który poziom opcjonalności reprezentuje to, co. Miałbym podwójne zdanie, jeśli napotkałem to w czyimś kodzie. Być może masz rację, że wydaje się to dziwne tylko dlatego, że jest niezwykłe, jednoznaczne. Być może nie jest to nic wyjątkowego dla funkcjonalnego programisty języka, z ich monadami i innymi. Chociaż sam bym tego nie wybrał, nadal dałem +1 twojej odpowiedzi, ponieważ to interesujące
Konrad Morawski
5

Wiem, że jestem spóźniony na imprezę, ale w każdym razie twój przypadek użycia przypomina, w jaki sposób JavaProperties pozwala również zdefiniować zestaw domyślnych właściwości, które zostaną sprawdzone, jeśli instancja nie załaduje odpowiedniego klucza.

Patrząc, jak odbywa się implementacja Properties.getProperty(String)(z Java 7):

Object oval = super.get(key);
String sval = (oval instanceof String) ? (String)oval : null;
return ((sval == null) && (defaults != null)) ? defaults.getProperty(key) : sval;

Jak już cytowałeś, naprawdę nie ma potrzeby „nadużywać wyjątków w celu kontroli przepływu”.

Podobnym, ale nieco bardziej zwięzłym podejściem do odpowiedzi @ BЈовић może być również:

public String getByKey(String key) {
    if (!valuesFromDatabase.containsKey(key)) {
        valuesFromDatabase.put(key, defaultValues.get(key));
    }
    return valuesFromDatabase.get(key);
}
hjk
źródło
4

Chociaż myślę, że odpowiedź @ BЈовић jest dobra na wypadek, gdyby nie getValueByKeybyła potrzebna nigdzie indziej, nie sądzę, aby twoje rozwiązanie było złe, gdyby Twój program zawierał oba przypadki użycia:

  • pobieranie według klucza z automatycznym tworzeniem na wypadek, gdyby klucz wcześniej nie istniał, oraz

  • pobieranie bez tego automatyzmu, bez zmiany czegokolwiek w bazie danych, repozytorium lub mapie klucza (pomyśl o getValueByKeybyciu publicznym, a nie prywatnym)

Jeśli taka jest Twoja sytuacja i dopóki poprawa wydajności jest akceptowalna, myślę, że proponowane rozwiązanie jest w pełni prawidłowe. Ma tę zaletę, że pozwala uniknąć powielania kodu pobierania, nie opiera się na zewnętrznych strukturach i jest dość prosty (przynajmniej w moich oczach).

W rzeczywistości w takiej sytuacji zależy od kontekstu, czy brakujący klucz jest sytuacją „wyjątkową”, czy nie. W kontekście, w którym się znajduje, getValueByKeypotrzebne jest coś podobnego . W kontekście, w którym oczekiwane jest automatyczne tworzenie klucza, zastosowanie metody, która ponownie wykorzystuje już dostępną funkcję, połyka wyjątek i zapewnia inne zachowanie po awarii, jest całkowicie sensowne. Można to interpretować jako rozszerzenie lub „dekorator” getValueByKey, nie tyle jako funkcję, w której „wyjątek jest nadużywany w celu kontroli”.

Oczywiście istnieje trzecia alternatywa: utwórz trzecią, prywatną metodę zwracającą Tuple<Boolean, String>, jak zasugerowałeś, i ponownie użyj tej metody zarówno w, jak getValueByKeyi getByKey. W przypadku bardziej skomplikowanego przypadku, który może być rzeczywiście lepszą alternatywą, ale w przypadku tak prostego pokazanego tutaj przykładu IMHO ma zapach nadinżynierii i wątpię, aby kod stał się w ten sposób łatwiejszy do utrzymania. Jestem tutaj z najwyższą odpowiedzią tutaj autorstwa Karla Bielefeldta :

„nie powinieneś czuć się źle przy korzystaniu z wyjątków, gdy upraszcza to twój kod”.

Doktor Brown
źródło
3

Optionaljest poprawnym rozwiązaniem. Jeśli jednak wolisz, istnieje alternatywa, która ma wrażenie „dwóch zer”, rozważ wartownika.

Zdefiniuj prywatny ciąg statyczny keyNotFoundSentinelo wartości new String(""). *

Teraz metoda prywatna może return keyNotFoundSentinelraczej throw new KeyNotFoundException(). Metoda publiczna może sprawdzić tę wartość

String rval = getValueByKey(key);
if (rval == keyNotFoundSentinel) { // reference equality, not string.equals
    ... // generate default value
} else {
    return rval;
}

W rzeczywistości nulljest szczególnym przypadkiem wartownika. Jest to wartosc wartosciowa zdefiniowana przez jezyk, z kilkoma szczególnymi zachowaniami (tj. Dobrze zdefiniowane zachowanie, jesli wywołuje sie metode null). Zdarza się, że taki wskaźnik jest tak przydatny, że używa go prawie każdy język.

* Celowo używamy, new String("")a nie tylko po to, ""aby zapobiec „internowaniu” łańcucha przez Javę, co dałoby mu takie samo odniesienie jak każdy inny pusty ciąg. Ponieważ wykonaliśmy ten dodatkowy krok, mamy gwarancję, że Ciąg, do którego keyNotFoundSentinelodnosi się, jest unikalną instancją, którą musimy upewnić się, że nigdy nie pojawi się na samej mapie.

Cort Ammon - Przywróć Monikę
źródło
To jest najlepsza odpowiedź IMHO.
fgp
2

Ucz się z frameworka, który nauczył się od wszystkich bolących punktów Javy:

.NET oferuje dwa bardziej eleganckie rozwiązania tego problemu, czego przykładem są:

Ten drugi jest bardzo łatwy do napisania w Javie, ten pierwszy wymaga jedynie klasy pomocnika „silnego odniesienia”.

Ben Voigt
źródło
DictionaryByłaby przyjazna dla kowariancji alternatywa dla tego wzoru T TryGetValue(TKey, out bool).
supercat