alternatywy dla zagnieżdżonych prób połowu w przypadku awarii

14

Mam sytuację, w której próbuję odzyskać obiekt. Jeśli wyszukiwanie nie powiedzie się, mam kilka awarii, z których każda może się nie powieść. Kod wygląda więc tak:

try {
    return repository.getElement(x);
} catch (NotFoundException e) {
    try {
        return repository.getSimilarElement(x);
    } catch (NotFoundException e1) {
        try {
            return repository.getParentElement(x);
        } catch (NotFoundException e2) {
            //can't recover
            throw new IllegalArgumentException(e);
        }
    }
}

To wygląda okropnie brzydko. Nienawidzę zwracać wartości zerowej, ale czy w tej sytuacji jest lepiej?

Element e = return repository.getElement(x);
if (e == null) {
    e = repository.getSimilarElement(x);
}
if (e == null) {
    e = repository.getParentElement(x);
}
if (e == null) {
    throw new IllegalArgumentException();
}
return e;

Czy są inne alternatywy?

Czy używanie zagnieżdżonych bloków try-catch jest anty-wzorem? jest powiązany, ale odpowiedzi są podobne do „czasami, ale zwykle można tego uniknąć”, nie mówiąc, kiedy i jak tego uniknąć.

Alex Wittig
źródło
1
Czy NotFoundExceptioncoś naprawdę jest wyjątkowego?
Nie wiem i prawdopodobnie dlatego mam problemy. Jest to w kontekście e-commerce, w którym produkty są wycofywane codziennie. Jeśli ktoś doda do zakładek produkt, który następnie zostanie wycofany, a następnie spróbuje otworzyć zakładkę ... czy to wyjątkowe?
Alex Wittig
@FiveNine moim zdaniem zdecydowanie nie - należy się tego spodziewać. Zobacz stackoverflow.com/questions/729379/...
Konrad Morawski

Odpowiedzi:

17

Zwykłym sposobem na wyeliminowanie zagnieżdżenia jest użycie funkcji:

Element getElement(x) {
    try {
        return repository.getElement(x);
    } catch (NotFoundException e) {
        return fallbackToSimilar(x);
    }  
}

Element fallbackToSimilar(x) {
    try {
        return repository.getSimilarElement(x);
     } catch (NotFoundException e1) {
        return fallbackToParent(x);
     }
}

Element fallbackToParent(x) {
    try {
        return repository.getParentElement(x);
    } catch (NotFoundException e2) {
        throw new IllegalArgumentException(e);
    }
}

Jeśli te zasady zastępcze są uniwersalne, można rozważyć wdrożenie tego bezpośrednio w repositoryobiekcie, w którym można użyć zwykłych ifinstrukcji zamiast wyjątku.

Karl Bielefeldt
źródło
1
W tym kontekście methodbyłoby lepszym słowem niż function.
Sulthan
12

Byłoby to naprawdę łatwe z czymś takim jak monada Option. Niestety Java nie ma takich. W Scali użyłbym tego Trytypu, aby znaleźć pierwsze udane rozwiązanie.

W moim sposobie myślenia o programowaniu funkcjonalnym stworzyłem listę wywołań zwrotnych reprezentujących różne możliwe źródła i przeglądałem je, dopóki nie znajdziemy pierwszego udanego:

interface ElementSource {
    public Element get();
}

...

final repository = ...;

// this could be simplified a lot using Java 8's lambdas
List<ElementSource> sources = Arrays.asList(
    new ElementSource() {
        @Override
        public Element get() { return repository.getElement(); }
    },
    new ElementSource() {
        @Override
        public Element get() { return repository.getSimilarElement(); }
    },
    new ElementSource() {
        @Override
        public Element get() { return repository.getParentElement(); }
    }
);

Throwable exception = new NoSuchElementException("no sources set up");
for (ElementSource source : sources) {
    try {
        return source.get();
    } catch (NotFoundException e) {
        exception = e;
    }
}
// we end up here if we didn't already return
// so throw the last exception
throw exception;

Może to być zalecane tylko wtedy, gdy naprawdę masz dużą liczbę źródeł lub jeśli musisz skonfigurować źródła w czasie wykonywania. W przeciwnym razie jest to niepotrzebna abstrakcja, a zyskasz więcej, utrzymując swój kod prosty i głupi, i po prostu wykorzystaj te brzydkie zagnieżdżone try-catch.

amon
źródło
+1 za wspomnienie o Trytypie w Scali, za wspomnienie o monadach i rozwiązanie za pomocą pętli.
Giorgio
Gdybym był już na Javie 8, wybrałbym to, ale jak powiedziałeś, to trochę za mało.
Alex Wittig
1
Do czasu opublikowania tej odpowiedzi Java 8 z obsługą Optionalmonady ( proof ) została już wydana.
mkalkov
3

Jeśli spodziewasz się, że wiele z tych wywołań repozytorium zostanie wyrzuconych NotFoundException, możesz użyć opakowania wokół repozytorium, aby usprawnić kod. Nie polecałbym tego do normalnych operacji, pamiętaj:

public class TolerantRepository implements SomeKindOfRepositoryInterfaceHopefully {

    private Repository repo;

    public TolerantRepository( Repository r ) {
        this.repo = r;
    }

    public SomeType getElement( SomeType x ) {
        try {
            return this.repo.getElement(x);
        }
        catch (NotFoundException e) {
            /* For example */
            return null;
        }
    }

    // and the same for other methods...

}
Rory Hunter
źródło
3

Zgodnie z sugestią @ amon, oto odpowiedź, która jest bardziej monadyczna. Jest to bardzo uproszczona wersja, w której musisz zaakceptować kilka założeń:

  • funkcja „unit” lub „return” jest konstruktorem klasy

  • operacja „bind” ma miejsce w czasie kompilacji, więc jest ukryta przed wywołaniem

  • funkcje „akcji” są również powiązane z klasą w czasie kompilacji

  • chociaż klasa jest ogólna i obejmuje dowolną klasę E, myślę, że w tym przypadku jest to przesada. Ale zostawiłem to w ten sposób jako przykład tego, co możesz zrobić.

Biorąc pod uwagę powyższe rozważania, monada przekłada się na płynną klasę opakowania (chociaż rezygnujesz z dużej elastyczności, którą uzyskałbyś w czysto funkcjonalnym języku):

public class RepositoryLookup<E> {
    private String source;
    private E answer;
    private Exception exception;

    public RepositoryLookup<E>(String source) {
        this.source = source;
    }

    public RepositoryLookup<E> fetchElement() {
        if (answer != null) return this;
        if (! exception instanceOf NotFoundException) return this;

        try {
            answer = lookup(source);
        }
        catch (Exception e) {
            exception = e;
        }

        return this;
    }

    public RepositoryLookup<E> orFetchSimilarElement() {
        if (answer != null) return this; 
        if (! exception instanceOf NotFoundException) return this;

        try {
            answer = lookupVariation(source);
        }
        catch (Exception e) {
            exception = e;
        }

        return this;
    }

    public RepositoryLookup<E> orFetchParentElement() {
        if (answer != null) return this; 
        if (! exception instanceOf NotFoundException) return this;

        try {
            answer = lookupParent(source);
        }
        catch (Exception e) {
            exception = e;
        }

        return this;
    }

    public boolean failed() {
        return exception != null;
    }

    public Exception getException() {
        return exception;
    }

    public E getAnswer() {
        // better to check failed() explicitly ;)
        if (this.exception != null) {
            throw new IllegalArgumentException(exception);
        }
        // TODO: add a null check here?
        return answer;
    }
}

(to się nie skompiluje ... niektóre szczegóły pozostają niedokończone, aby próbka była mała)

I wywołanie wyglądałoby tak:

Repository<String> repository = new Repository<String>(x);
repository.fetchElement().orFetchParentElement().orFetchSimilarElement();

if (repository.failed()) {
    throw new IllegalArgumentException(repository.getException());
}

System.err.println("Got " + repository.getAnswer());

Pamiętaj, że możesz dowolnie komponować operacje „pobierania”. Zatrzyma się, gdy otrzyma odpowiedź lub wyjątek inny niż nie znaleziony.

Zrobiłem to naprawdę szybko; nie jest to w porządku, ale mam nadzieję, że przekazuje ten pomysł

Obrabować
źródło
1
repository.fetchElement().fetchParentElement().fetchSimilarElement();- moim zdaniem: zły kod (w sensie podanym przez Jona Skeeta)
Konrad Morawski
niektórzy ludzie nie lubią tego stylu, ale używanie return thisdo tworzenia wywołań obiektów łańcuchowych istnieje już od dawna. Ponieważ OO obejmuje obiekty zmienne, return thisjest mniej więcej równoważne return nullbez łączenia łańcuchów. Jednakże, return new Thing<E>otwiera drzwi do innej możliwości, że ten przykład nie przechodzi w tak ważne dla tego wzorca, jeśli zdecydujesz się pójść tą drogą.
Rob
1
Ale podoba mi się ten styl i nie jestem przeciwny łańcuchowym połączeniom ani płynnym interfejsom jako takim. Istnieje jednak różnica między CustomerBuilder.withName("Steve").withID(403)tym kodem, ponieważ sam widok .fetchElement().fetchParentElement().fetchSimilarElement()nie jest jasne, co się dzieje, i to jest kluczowe. Czy wszyscy zostali przyprowadzeni? W tym przypadku nie kumuluje się, a zatem nie jest tak intuicyjny. Muszę to zobaczyć, if (answer != null) return thiszanim naprawdę to zrozumiem. Być może jest to tylko kwestia właściwego nazewnictwa ( orFetchParent), ale i tak jest to „magia”.
Konrad Morawski
1
Przy okazji (wiem Twój kod jest uproszczony i tylko proof of concept), dobrze byłoby, aby może powrócić klon answerw getAnsweri resetu (wyczyść) answerSamo pole przed powrotem jego wartość. W przeciwnym razie łamie to zasadę rozdzielania poleceń / zapytań, ponieważ żądanie pobrania elementu (zapytania) zmienia stan obiektu repozytorium ( answernigdy nie jest resetowany) i wpływa na zachowanie fetchElementnastępnego wywołania. Tak, trochę podrywam, myślę, że odpowiedź jest prawidłowa, nie byłem tym, który ją ocenił.
Konrad Morawski
1
trafne spostrzeżenie. Innym sposobem może być „próba pobrania ...”. Ważną kwestią jest to, że w tym przypadku wywoływane są wszystkie 3 metody, ale w innym przypadku klient może po prostu użyć „tryFetch (). PróbaFetchParent ()”. Nazywanie go „Repozytorium” jest błędne, ponieważ tak naprawdę modeluje pojedyncze pobranie. Być może będę miał problemy z nazwami, aby nazwać to „RepositoryLookup” i „próbować”, aby wyjaśnić, że jest to jednorazowy, przejściowy artefakt, który zapewnia pewną semantykę wokół wyszukiwania.
Rob
2

Innym sposobem ustrukturyzowania szeregu takich warunków jest noszenie flagi lub testowanie wartości zerowej (jeszcze lepiej, użyj Opcji Guava, aby określić, kiedy jest dostępna dobra odpowiedź), aby połączyć warunki razem.

Element e = null;

try {
    e = repository.getElement(x);
} catch (NotFoundException e) {
    // nope -- try again!
}

if (e == null) {  // or ! optionalElement.isPresent()
    try {
        return repository.getSimilarElement(x);
    } catch (NotFoundException e1) {
        // nope -- try again!
    }
}

if (e == null) {  // or ! optionalElement.isPresent()
    try {
        return repository.getParentElement(x);
    } catch (NotFoundException e2) {
        // nope -- try again!
    }
}

if (e == null) {  // or ! optionalElement.isPresent()
    //can't recover
    throw new IllegalArgumentException(e);
}

return e;

W ten sposób obserwujesz stan elementu i wykonujesz odpowiednie wywołania na podstawie jego stanu - to znaczy dopóki nie masz jeszcze odpowiedzi.

(Zgadzam się jednak z @amon. Polecam przyjrzeć się wzorowi Monad, z takim obiektem opakowującym, class Repository<E>który ma członków E answer;i Exception error;. Na każdym etapie sprawdzaj, czy jest wyjątek, a jeśli tak, pomiń każdy pozostały krok. koniec, masz albo odpowiedź, brak odpowiedzi, albo wyjątek i możesz zdecydować, co z tym zrobić.)

Obrabować
źródło
-2

Po pierwsze, wydaje mi się, że powinna istnieć taka funkcja repository.getMostSimilar(x) (powinieneś wybrać bardziej odpowiednią nazwę), ponieważ wydaje się, że istnieje logika służąca do znajdowania najbliższego lub najbardziej podobnego elementu dla danego elementu.

Repozytorium może następnie zaimplementować logikę pokazaną w poście amons. Oznacza to, że jedynym przypadkiem wyjątku jest zgłoszenie wyjątku, gdy nie można znaleźć żadnego pojedynczego elementu.

Jest to jednak oczywiście możliwe tylko wtedy, gdy logika znajdowania najbliższego elementu może być zawarta w repozytorium. Jeśli nie jest to możliwe, proszę podać więcej informacji, w jaki sposób (według jakich kryteriów) można wybrać najbliższy element.

walenteria
źródło
odpowiedzi mają odpowiedzieć na pytanie, a nie prosić o wyjaśnienia
komnata
Cóż, moja odpowiedź rozwiązuje jego problem, ponieważ pokazuje sposób uniknięcia zagnieżdżonego try / catch w określonych warunkach. Tylko w przypadku niespełnienia tych warunków potrzebujemy więcej informacji. Dlaczego to nie jest poprawna odpowiedź?
valenterry