我正在尝试实现用于处理事件的验证模块。验证模块基于简单的界面:
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!")
}
}
这件作品看起来很好,但我对它的样子感到不舒服。我最关心的是将整个流处理放在一次尝试中是否是一种好的做法。此外,我不认为这种例外捕获+返回选项的混合是优雅的。
对于这种情况,我可以使用一些建议和/或最佳实践。
2条答案
按热度按时间zmeyuzjn1#
但我对它的样子感到不舒服。
你使用的api是疯狂的设计。处理愚蠢API的方法通常是相同的:
尝试“上游”修复:提出拉取请求,与提出请求的团队交谈,等等。
如果并且只有在这个选项已经用尽的情况下,那么[a]编写任何你必须编写的丑陋的黑客程序,使其工作,[b]将丑陋的代码限制在尽可能小的一段代码中;这可能需要编写一个“包含”难看内容的 Package 器,最后[c]不要担心在受限的“这里难看是可以的”区域内的代码优雅性。
这个api之所以奇怪,是因为它既把验证搞错了,又没有利用他们的错误带来的好处(比如,如果我错认为他们的方法是错误的,那么至少他们在他们的方法上没有做得最好)。
特别地,异常是一个返回值,从某种意义上说,它是一种从方法返回的方法。为什么界面不是:
更一般地说,验证不是一种“最多有一件事情出错”的情况,这会导致你的问题是“围绕整个事情写一个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,正确的代码风格的经验法则就消失了。
是的,这是一个好主意-虽然api希望您不要抛出异常,而是将它们 Package 在可选项中,但这一部分是不好的,而且您通常不应该使错误永久化,即使这意味着您的代码开始在样式上有所不同。
event.getToys().forEach(this::validateSingleToy);
一般来说,使用forEach
方法,或.stream().forEach()
,是一种代码气味。foreach只能用于两种情况:这是一堆溪流行动的终点站(
.stream().filter().flatMap().map()....forEach
-那就好了)。你已经有一个
Consumer<T>
对象并希望它为列表中的每个元素运行。你两个都没有。此代码最好写为:
lambda有3个缺点(如果完全按照预期使用lambda,即作为可能在某些不同上下文中运行的代码,这些缺点就会变成优点):
控制流不透明。
不可变的局部变量透明。
未选中异常类型透明。
你失去了三样东西,却得不到任何回报。当有两种同样简洁明了的方法来做同一件事,但其中一种方法适用于严格的场景超集时,一定要用超集风格来写,因为代码一致性是一个值得追求的目标,这会导致更大的一致性(这是值得的,因为它减少了风格摩擦,降低了学习曲线)。
这条规则在这里适用。
lnxxn5zx2#
返回异常而不是返回异常很奇怪,但不管怎样(为什么不退一张
ValidationResult
而不是对象?异常通常是用来抛出和捕获的)。但你可以改变你的私人方法也返回
Optional
使它们更容易组合的示例。它也可以避免混合扔和返回和流。不确定这是不是你要找的?