альтернативы вложенным try-catch для резервных вариантов

14

У меня есть ситуация, когда я пытаюсь получить объект. Если поиск не удался, у меня есть несколько запасных вариантов, каждый из которых может дать сбой. Итак, код выглядит так:

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);
        }
    }
}

Это выглядит ужасно уродливо. Я ненавижу возвращать ноль, но разве это лучше в этой ситуации?

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;

Есть ли другие альтернативы?

Является ли использование вложенных блоков try-catch антишаблоном? связан, но ответы там по типу «иногда, но обычно этого можно избежать», не говоря, когда или как этого избежать.

Алекс Виттиг
источник
1
Это NotFoundExceptionто, что на самом деле является исключительным?
Я не знаю, и, вероятно, поэтому у меня проблемы. Это в контексте электронной коммерции, где продукты больше не выпускаются ежедневно. Если кто-то делает закладку на продукт, который впоследствии прекращается, а затем пытается открыть закладку ... это исключение?
Алекс Виттиг
@FiveNine, на мой взгляд, определенно нет - это следовало ожидать. См. Stackoverflow.com/questions/729379/…
Конрад Моравский

Ответы:

17

Обычный способ устранения вложенности заключается в использовании функций:

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);
    }
}

Если эти резервные правила универсальны, вы можете рассмотреть возможность реализации этого непосредственно в repositoryобъекте, где вы могли бы просто использовать простые ifвыражения вместо исключения.

Карл Билефельдт
источник
1
В этом контексте methodбыло бы лучше, чем слово function.
Sulthan
12

Это было бы действительно легко с чем-то вроде монады Option. К сожалению, у Java их нет. В Scala я бы использовал Tryтип, чтобы найти первое успешное решение.

В своем мышлении функционального программирования я настраивал список обратных вызовов, представляющих различные возможные источники, и перебирал их, пока не нашел первый успешный:

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;

Это может быть рекомендовано, только если у вас действительно много источников, или если вы должны настроить источники во время выполнения. В противном случае это ненужная абстракция, и вы получите больше пользы от сохранения вашего кода простым и глупым и просто будете использовать эти уродливые вложенные try-catch.

Амон
источник
+1 за упоминание Tryтипа в Scala, за упоминание монад и за решение с использованием цикла.
Джорджио
Если бы я уже был на Java 8, я бы пошел на это, но, как вы говорите, это немного для нескольких откатов.
Алекс Виттиг
1
На самом деле, к тому времени, когда этот ответ был опубликован, Java 8 с поддержкой Optionalмонады ( доказательства ) уже была выпущена.
mkalkov
3

Если вы ожидаете, что многие из этих вызовов репозитория будут выбрасываться NotFoundException, вы можете использовать обертку вокруг репозитория, чтобы упростить код. Я бы не рекомендовал это для нормальной работы:

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...

}
Рори Хантер
источник
3

По предложению @ amon вот более монадический ответ. Это очень упрощенная версия, в которой вы должны принять несколько предположений:

  • функция "unit" или "return" является конструктором класса

  • операция связывания происходит во время компиляции, поэтому она скрыта от вызова

  • функции "action" также связаны с классом во время компиляции

  • хотя класс является универсальным и охватывает любой произвольный класс E, я думаю, что в данном случае это на самом деле избыточно. Но я оставил это как пример того, что вы могли бы сделать.

Исходя из этих соображений, монада превращается в плавный класс-обертку (хотя вы теряете большую гибкость, которую получаете в чисто функциональном языке):

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;
    }
}

(это не скомпилируется ... некоторые детали остаются незавершенными, чтобы сохранить размер выборки)

И вызов будет выглядеть так:

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());

Обратите внимание, что у вас есть возможность составлять операции «извлечения» так, как вам нравится. Он остановится, когда получит ответ или исключение, отличное от не найденного.

Я сделал это очень быстро; это не совсем правильно, но, надеюсь, передает идею

обкрадывать
источник
1
repository.fetchElement().fetchParentElement().fetchSimilarElement();- по моему мнению: злой кодекс (в том смысле, который дан Джоном Скитом)
Конрад Моравский
некоторым людям не нравится этот стиль, но использование return thisдля создания вызовов объектов цепочки уже давно. Поскольку ОО включает изменяемые объекты, return thisэто более или менее эквивалентно return nullбез цепочки. Тем не менее, return new Thing<E>открывается дверь к другой возможности, в которую этот пример не входит, поэтому для этого шаблона важно, если вы решите пойти по этому пути.
Роб
1
Но мне нравится этот стиль, и я не против объединения вызовов или беглых интерфейсов как таковых. Однако есть разница между CustomerBuilder.withName("Steve").withID(403)этим и этим кодом, потому что просто .fetchElement().fetchParentElement().fetchSimilarElement()неясно, что происходит, и это ключевой момент. Они все забраны? Это не накапливается в этом случае, и, следовательно, не настолько интуитивно понятен. Мне нужно увидеть это, if (answer != null) return thisпрежде чем я действительно получу это. Возможно, это просто вопрос правильного именования ( orFetchParent), но в любом случае это «волшебство».
Конрад Моравский
1
Между прочим (я знаю, что ваш код упрощен и является лишь доказательством концепции), было бы хорошо, возможно, вернуть клон answerin getAnswerи сбросить (очистить) само answerполе перед возвратом его значения. Иначе это как бы нарушает принцип разделения команды / запроса, потому что запрос на выбор элемента (запрос) изменяет состояние вашего объекта репозитория ( answerникогда не сбрасывается) и влияет на поведение fetchElementпри следующем вызове. Да, я немного придираюсь, я думаю, что ответ верен, я не был тем, кто отрицал это.
Конрад Моравский
1
неплохо подмечено. Другим способом было бы "попытка загрузки ...". Важным моментом является то, что в этом случае вызываются все 3 метода, но в другом случае клиент может просто использовать «tryFetch (). TryFetchParent ()». Кроме того, называть его «Репозиторий» неправильно, потому что он действительно моделирует одну выборку. Возможно, я возню с именами, которые будут называться «RepositoryLookup» и «попытка» прояснить, что это временный артефакт, который обеспечивает определенную семантику при поиске.
Роб
2

Другой способ структурировать ряд условий, подобных этому, состоит в том, чтобы пометить флаг или проверить нулевое значение (еще лучше, если вы используете опцию Guava Optional для определения наличия хорошего ответа), чтобы связать условия в единое целое.

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;

Таким образом, вы наблюдаете за состоянием элемента и делаете правильные вызовы, основываясь на его состоянии, то есть до тех пор, пока у вас еще нет ответа.

(Однако я согласен с @amon. Я бы рекомендовал посмотреть шаблон Monad с таким объектом-оберткой, class Repository<E>который имеет члены E answer;и Exception error;. На каждом этапе проверяйте, есть ли исключение, и если да, пропустите каждый оставшийся шаг. в конце концов, у вас остается либо ответ, либо отсутствие ответа, либо исключение, и вы можете решить, что с этим делать.)

обкрадывать
источник
-2

Во-первых, мне кажется, что должна существовать функция, подобная repository.getMostSimilar(x)(вы должны выбрать более подходящее имя), поскольку существует логика, которая используется для нахождения ближайшего или наиболее похожего элемента для данного элемента.

Затем репозиторий может реализовать логику, как показано в посте amons. Это означает, что единственный случай, когда должно быть сгенерировано исключение, - это когда нет единственного элемента, который можно найти.

Однако это, конечно, возможно только в том случае, если логика для поиска ближайшего элемента может быть инкапсулирована в хранилище. Если это невозможно, предоставьте больше информации о том, как (по каким критериям) можно выбрать ближайший элемент.

valenterry
источник
ответы должны отвечать на вопрос, а не запрашивать разъяснения
комнат
Ну, мой ответ - решить его проблему, поскольку он показывает способ избежать вложенных попыток / ловли при определенных условиях. Только если эти условия не выполняются, нам нужно больше информации. Почему это неверный ответ?
valenterry