Java POJO是否应该在setter方法中进行字段验证并抛出异常?

18
假设我们有数十个 Java POJO,它们代表我的领域,也就是在我系统中以对象的形式在不同的层之间流动的数据。系统可以是 Web 应用程序或简单桌面应用程序。领域包含的内容并不重要。
在设计系统时,我很困惑应该把验证逻辑放在哪里。我的 POJO(领域对象)代表我的数据,其中一些字段必须符合特定的标准,但如果我在 setter 方法中放置了大量验证逻辑,那么告诉调用客户端的唯一方法就是抛出异常。如果我不想让系统崩溃,那么异常必须是一个已检查的异常,必须被捕获和处理。其结果是每次使用 setter 方法创建新对象(甚至是构造函数)时,我都必须重新抛出异常或使用 try-catch 块。被迫在许多 setter 方法上使用 try-catch 感觉不对劲。
所以问题是我应该把验证逻辑放在哪里,这样我就不会用大量的样板 try-catch 块和重新抛出来弄乱我的代码。欢迎最好的 JAVA 字节吞噬者加入讨论。
我已经研究和搜索过了,但没有找到关于此主题的具体讨论,所以我非常热切地等待深入了解应该如何做的更多内部信息。

你有检查过Java Bean验证吗?http://docs.oracle.com/javaee/6/tutorial/doc/gircz.html - suman j
尝试使用Bean验证(JSR-303),由hibernate-validator实现。 - beerbajay
是的,我已经查看了那些概念,但我不是在询问特定的API或方法。我想要理解并思考应该如何验证数据,以及验证应该放在哪里,以避免在整个系统中使用大量异常和异常处理。 - Gogi
在设计API时,我认为尽早验证公共方法的输入最好 - 没有必要存储或处理违反任何文档约定的输入。这种违规被认为是编程错误,并可能导致不受检查的异常,例如IllegalArgumentExceptionUnsupportedOperationException或NPE。这可以防止“垃圾进入 - 垃圾输出”的情况并警告调用者他们正在做错事,而_无需_将代码弄乱异常处理。 - fspinnenhirn
1
可以说,拥有这样的验证逻辑是拥有setter方法的原因:https://dev59.com/iXM_5IYBdhLWcg3wPAnU#1461662 - Raedwald
另请参阅https://dev59.com/oHI_5IYBdhLWcg3wAeHl#12108025 - Raedwald
4个回答

16

当你说

一些对象内的字段必须符合某些标准

你或许已经回答了自己的问题。思考系统中的不变量(invariants),即你想要维护的东西,或是必须遵循的规则,总是有帮助的。
你的POJOs是保证数据对象中这些不变量的“最后一道防线”,因此将验证逻辑放在其中是合适的,甚至是必要的。如果没有这样的验证,一个对象可能不再代表你的领域中有意义的东西。

这些系统不变量构成了你的对象(或方法)与它们的“客户”之间的契约。如果有人试图违反这个(希望是精心记录的)契约,那么抛出异常是正确的做法,因为使用系统的各个部分是客户的责任。

随着时间的推移,我开始更喜欢使用未检查的异常而不是已检查的异常来处理契约违规的任何情况,部分原因是你提到的原因,即避免在try-catch块中无处不在。
Java的标准未检查异常包括:

  • NullPointerException
  • IllegalArgumentException
  • IllegalStateException
  • UnsupportedOperationException

最佳实践指南是在错误被认为是可恢复的时候使用已检查异常,否则使用未检查异常

Joshua Bloch的《Effective Java, 2nd ed.》第9章就这个主题提供了更多智慧:

  • Item 57:仅针对异常情况使用异常
  • Item 58:对于可恢复的条件使用已检查异常,对于编程错误使用运行时异常
  • Item 59:避免不必要地使用已检查异常
  • Item 60:偏好使用标准异常

以上任何内容都不应妨碍您在更高层次上使用适当的验证逻辑,特别是强制执行任何特定于上下文的业务规则或约束条件。


是的,POJO必须始终处于有效状态。我确实研究了《Effective Java》中的条款,但它们有点难以理解。我认为对于那些了解OO设计原则但无法正确决定如何应用这些条款的中级开发人员来说,它们肯定会显得困难。例如,我不明白他具体指什么“可恢复”和“编程错误”。一些具体且可能不同的示例可以帮助我们理解这些建议背后的强大思想。他所说的“不必要”,是指我们有没有任何众所周知的例子呢? - Gogi
我认为Bloch需要仔细阅读,而且您的疑虑本身就是很好的SO问题。编程错误是违反合同的事情,例如在文档中说明不支持传递null或负索引。可恢复错误的一个例子可能是超时,调用者可以决定稍后重试或放弃;使用检查异常强制调用者处理此问题并做出决策。使用检查异常来控制流程而不是使用IF-ELSE块是不必要的使用示例。 - fspinnenhirn

3
总之,我认为没有一种独特的解决方案适用于所有需求,最终取决于您的情况和偏好。
从封装的角度来看,我认为设置器验证是正确的方法,因为它是确定所提供信息是否正确并提供详细说明可能有什么问题的逻辑位置。但是,我不确定您所说的意思是什么:
“如果我不想让系统崩溃,异常必须是已检查异常......”
为什么系统会崩溃?未经检查的异常可以像已检查的异常一样非常好地被捕获。您需要找出程序在发生此类事件时应该如何行事,以便决定在哪里捕获它们以及要执行什么操作。
已检查与未检查一直以来都在各种方式和信仰上进行辩论,但我认为没有理由不抛出未检查异常。只需创建一个常见的“ConfigurationException”(或使用已经存在的“IllegalArgumentException”),相应地标记方法签名并添加适当的Java文档,以便调用它们的人知道要期望什么,并在需要时抛出。

根据你的对象关系和层次结构,另一种解决方案可能是一些构建器,它们仅在创建实例时运行你的自定义验证。但正如我所说,这真的取决于情况,你可能无法阻止其他人手动实例化并不正确地填充某些对象。


同意使用建造者模式(+1),当我读到这个问题时,这就是我立刻想到的。 - Daniel Pryden

1
在某些情况下,解决方案是将所有的设置器设为私有,并提供一个单一的入口点来初始化对象。然后,在该初始化方法中处理所有的验证和异常处理。这也确保没有对象可以部分初始化。对象状态的更改也可以通过这种方式来处理,即除了构造函数/初始化方法之外,使对象不变,但如果滥用会变得浪费。

1

虽然您可以在bean的setter方法上放置验证逻辑,但我发现实践中,更清晰的关注点分离是将任何复杂验证移动到专门用于验证的另一个类中。听我说。

使用bean验证很好,但在许多情况下(我确定您现在正在遇到这些情况),简单的注释将无法进行彻底的验证。像Spring这样的框架有Validator,您可以实现它来执行此操作。

将验证逻辑分离的好处有很多:

  • 代码重用。在某些情况下,一个bean的setter验证可能与另一个bean的setter验证完全相同。这也适用于更简洁的单元测试。
  • 在许多情况下,您根本不需要处理异常,而是向用户显示错误消息
  • 您可以适当地记录日志,而无需填充POJOs日志语句或强制它们实现日志记录器
  • 代码更可读,意图更易于理解

希望这有所帮助。


你提到了一些重要的点,特别是日志记录、可读性和关注点分离。你能否举出一些真正“深入”和现实的例子,以便更好地理解? - Gogi
如果您需要更具体的示例,我之后可以提供一份,但这应该足够:http://www.journaldev.com/2668/spring-mvc-form-validation-example-using-annotation-and-custom-validator-implementation。正如您所看到的,在setter方法中没有验证。这种设计已经使用了很长时间,并在生产环境中使用。如果链接以后失效,可以谷歌搜索Spring Validator的例子获得更多案例。 - riddle_me_this

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接