嵌套try-catches的替代方案,用于后备


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

这看起来很难看。我讨厌返回null,但是在这种情况下会更好吗?

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的东西,实际上是例外?

我不知道,这可能就是为什么我遇到麻烦了。这是在电子商务环境中,每天都会停产产品。如果有人为某个产品添加了书签,该产品随后停产,然后尝试打开该书签...那是例外吗?
Alex Wittig 2014年

我认为@FiveNine绝对不是-这是可以预期的。请参阅stackoverflow.com/questions/729379/…–
康拉德·莫拉夫斯基

Answers:


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)2014年

12

使用Option monad之类的东西真的很容易。不幸的是,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表示TryScala中的类型,提及monad,以及使用循环的解决方案。
Giorgio

如果我已经在使用Java 8,那么我会继续这样做,但是就像您说的那样,这只是回退一些。
Alex Wittig 2014年

1
实际上,在发布此答案时,已经发布了支持Optionalmonad(证明)的Java 8 。
mkalkov 2015年

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”函数是类的构造函数

  • “绑定”操作在编译时发生,因此对调用隐藏

  • “动作”功能在编译时也绑定到该类

  • 尽管该类是泛型的,并且包装了任意的E类,但在这种情况下,我认为这实际上是多余的。但是我以这种方式保留了它,作为您可以做什么的示例。

考虑到这些因素,monad可以转换为流畅的包装器类(尽管您放弃了纯函数式语言所具有的很多灵活性):

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();-我认为:邪恶的密码(按Jon Skeet的
说法

有些人不喜欢这种样式,但是return this用于创建链接对象调用的方法已经存在很长时间了。由于OO涉及可变对象,return this因此或多或少等同于return null不链接。但是,return new Thing<E>这为该示例没有涉及的另一项功能打开了大门,因此,如果您选择沿这条路走,则对于此模式而言很重要。
罗布2014年

1
但是我喜欢这种风格,并且我不反对这样的链接调用或流畅的接口。但是CustomerBuilder.withName("Steve").withID(403),此代码与此代码之间是有区别的,因为仅凭一眼.fetchElement().fetchParentElement().fetchSimilarElement()就不清楚发生了什么,这就是这里的关键。他们都被拿走了吗?在这种情况下,它不是累积性的,因此不是那么直观。我必须先看到这一点,if (answer != null) return this然后才能真正理解它。也许这只是适当命名的问题(orFetchParent),但无论如何都是“神奇的”。
Konrad Morawski 2014年

1
顺便说一句(我知道您的代码被简化了,只是一个概念上的证明),最好返回answerin 的克隆getAnsweranswer在返回其值之前重置(清除)字段本身。否则,这会破坏命令/查询分离的原理,因为要求获取元素(查询)会更改存储库对象的状态(answer永远不会重置)并影响fetchElement下次调用它时的行为。是的,我有点挑剔,我认为答案是正确的,我不是反对的人。
Konrad Morawski 2014年

1
那是个很好的观点。另一种方法是“ attemptToFetch ...”。重要的一点是,在这种情况下,将调用所有3个方法,但在另一种情况下,客户端可能只使用“ attemptFetch()。attemptFetchParent()”。另外,将其称为“存储库”是错误的,因为它实际上是在模拟单个提取。也许我会大惊小怪地将其命名为“ RepositoryLookup”和“ attempt”,以明确这是一个一次性的临时工件,提供了围绕查找的某些语义。
罗布

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帖子中所示的逻辑。这意味着,唯一的例外情况是当找不到单个元素时抛出异常。

但是,这当然只有在能够找到最接近元素的逻辑可以封装到存储库中的情况下才可能。如果无法做到这一点,请提供更多信息,说明如何(根据哪个标准)选择最接近的元素。


答案是回答问题,而不是要求澄清
gnat 2014年

好吧,我的回答是解决他的问题,因为它显示了一种避免在某些情况下嵌套的try / catching的方法。只有不满足这些条件,我们才需要更多信息。为什么这不是一个有效的答案?
valenterry 2014年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.