java—返回可选值,具体取决于流中使用的方法引发的异常

eblbsuwk  于 2021-07-14  发布在  Java
关注(0)|答案(2)|浏览(309)

我正在尝试实现用于处理事件的验证模块。验证模块基于简单的界面:

public interface Validator {    
    Optional<ValidationException> validate(Event event);
}

我团队中现有的代码库依赖于 Package 异常机制—我不能真正使用它。我在实现新的验证器时遇到了问题,该验证器负责从两个方面验证单个事件。
假设事件是 PlayWithDogEvent ,它包含 Toys 狗可以和你一起玩。
此类事件的验证流程:
对于每个玩具,
看看是不是球
如果是个球,就不要太大。
如果任何玩具不是一个球/太大的球,我的 validate(Event event) 方法应返回 Optional.of(new ValidationException("some msg")) .
我通过以下方式实现了我的验证器:

public class ValidBallsOnlyValidator implements Validator {

    @Override
    public Optional<ValidationException> validate(Event event) {        
        try {
            event.getToys().forEach(this::validateSingleToy);
            return Optional.empty();
        } catch (InvalidToyException ex) {
            return Optional.of(new ValidationException(ex.getMessage()));
        }       
    }

    private void validateSingleToy(Toy toy) {
        // In real code the optional here is kinda mandatory
        Optional<Toy> potentialBall = castToyToBall(toy);
        // Im using Java 8
        if(potentiallBall.isPresent()) {
            checkIfBallIsOfValidSize(potentialBall.get(), "exampleSize");
        } else {
            throw new InvalidToyException("The toy is not a ball!")
        }
    }

    private void checkIfBallIsOfValidSize(Toy toy, String size) {
        if(toyTooLarge(toy, size)) throw new InvalidToyException("The ball is too big!")
    }

}

这件作品看起来很好,但我对它的样子感到不舒服。我最关心的是将整个流处理放在一次尝试中是否是一种好的做法。此外,我不认为这种例外捕获+返回选项的混合是优雅的。
对于这种情况,我可以使用一些建议和/或最佳实践。

zmeyuzjn

zmeyuzjn1#

但我对它的样子感到不舒服。
你使用的api是疯狂的设计。处理愚蠢API的方法通常是相同的:
尝试“上游”修复:提出拉取请求,与提出请求的团队交谈,等等。
如果并且只有在这个选项已经用尽的情况下,那么[a]编写任何你必须编写的丑陋的黑客程序,使其工作,[b]将丑陋的代码限制在尽可能小的一段代码中;这可能需要编写一个“包含”难看内容的 Package 器,最后[c]不要担心在受限的“这里难看是可以的”区域内的代码优雅性。
这个api之所以奇怪,是因为它既把验证搞错了,又没有利用他们的错误带来的好处(比如,如果我错认为他们的方法是错误的,那么至少他们在他们的方法上没有做得最好)。
特别地,异常是一个返回值,从某种意义上说,它是一种从方法返回的方法。为什么界面不是:

public interface Validator {    
    void validate(Event event) throws ValidationException;
}

更一般地说,验证不是一种“最多有一件事情出错”的情况,这会导致你的问题是“围绕整个事情写一个try/catch感觉很奇怪”。
很多事情都可能出错。可能有5个玩具,其中一个是球,但太大,其中一个是吱吱作响的玩具。只报告一个错误(可能是任意选择的错误)是很奇怪的。
如果您不打算抛出验证异常,而是返回验证问题,那么问题首先应该不是异常,而是其他对象,并且,您应该使用 List<ValidationIssue> 而不是一个 Optional<ValidationIssue> . 你已经摆脱了一个可选的,这总是一个胜利,你现在可以处理多个问题,在一次去。如果处理所有这些问题的“终点”根本上不能同时处理多个问题,那没关系:他们可以将该列表视为一个有效的可选选项,并且 list.isEmpty() 作为“一切正常”的指标,以及 list.get(0) 否则用于获取第一个问题(这是这个一次出错的系统能够处理的唯一问题)。
这涉及到代码优雅,这是定义“优雅”一词的唯一有意义的方法:代码更容易测试、更容易理解和更灵活。它更灵活:如果稍后处理验证错误的端点代码被更新为能够处理多个错误,那么现在您就可以这样做,而不必触及生成验证问题对象的代码。
因此,重写这一切。或者:
使api设计的重点是抛出该异常,而不是将其推入可选的,-或-
使api列表为基础,也摆脱可选的(耶!)可能不适用于验证问题对象 extends SomeException . 如果你不想扔掉它,就别让它成为一个可以扔掉的东西。
如果这不好的话,主要是不要太担心优雅——一旦你被迫使用设计糟糕的API,优雅就不在考虑之列了。
然而,几乎总是有一些样式注解可以提供给任何代码。 return Optional.of(new ValidationException(ex.getMessage())); 通常,这是非常糟糕的异常处理,您的linter工具应该将其标记为不可接受。如果 Package 异常,则希望保留原因以保留堆栈跟踪和任何特定于异常类型的信息。你忽略了所有关于你的事情,从而摆脱了这一切 ex ,除了它的消息。通常,这应该是 new ValidationException("Some string that adds appropriate context", ex) -从而保留了链条。如果没有要添加的上下文/很难想象这可能是什么,那么您根本不应该 Package ,而是继续抛出原始异常。
然而,考虑到异常在这里被滥用,也许这段代码是可以的——这又回到了中心点:一旦你致力于使用设计糟糕的api,正确的代码风格的经验法则就消失了。

private void checkIfBallIsOfValidSize(Toy toy, String size) {
       if(toyTooLarge(toy, size)) throw new InvalidToyException("The ball is too big!")
}

是的,这是一个好主意-虽然api希望您不要抛出异常,而是将它们 Package 在可选项中,但这一部分是不好的,而且您通常不应该使错误永久化,即使这意味着您的代码开始在样式上有所不同。 event.getToys().forEach(this::validateSingleToy); 一般来说,使用 forEach 方法,或 .stream().forEach() ,是一种代码气味。foreach只能用于两种情况:
这是一堆溪流行动的终点站( .stream().filter().flatMap().map()....forEach -那就好了)。
你已经有一个 Consumer<T> 对象并希望它为列表中的每个元素运行。
你两个都没有。此代码最好写为:

for (var toy : event.getToys()) validateSingleToy(toy);

lambda有3个缺点(如果完全按照预期使用lambda,即作为可能在某些不同上下文中运行的代码,这些缺点就会变成优点):
控制流不透明。
不可变的局部变量透明。
未选中异常类型透明。
你失去了三样东西,却得不到任何回报。当有两种同样简洁明了的方法来做同一件事,但其中一种方法适用于严格的场景超集时,一定要用超集风格来写,因为代码一致性是一个值得追求的目标,这会导致更大的一致性(这是值得的,因为它减少了风格摩擦,降低了学习曲线)。
这条规则在这里适用。

lnxxn5zx

lnxxn5zx2#

返回异常而不是返回异常很奇怪,但不管怎样(为什么不退一张 ValidationResult 而不是对象?异常通常是用来抛出和捕获的)。
但你可以改变你的私人方法也返回 Optional 使它们更容易组合的示例。它也可以避免混合扔和返回和流。不确定这是不是你要找的?

public class ValidBallsOnlyValidator implements Validator {

    @Override
    public Optional<ValidationException> validate(Event event) 
        return event.getToys()
                .stream()
                .filter(Optional::isPresent)
                .findFirst()
                .map(ex -> new ValidationException(ex.getMessage()));
    }

    private Optional<InvalidToyException> validateSingleToy(Toy toy) {
        // In real code the optional here is kinda mandatory
        Optional<Toy> potentialBall = castToyToBall(toy);
        if(potentiallBall.isPresent()) {
            return checkIfBallIsOfValidSize(potentialBall.get(), "exampleSize");
        } else {
            return Optional.of(new InvalidToyException("The toy is not a ball!"));
        }
    }

    private Optional<InvalidToyException> checkIfBallIsOfValidSize(Toy toy, String size) {
        if(toyTooLarge(toy, size)) return Optional.of(new InvalidToyException("The ball is too big!"));
        return Optional.empty();
    }

}

相关问题