问题
如何最好地管理需要复杂验证逻辑的对象图的构造?出于可测试性原因,我想保留依赖注入,无操作构造函数。
可测试性对我来说非常重要,您的建议如何维护代码的这个属性?
背景
我有一个普通的java对象,它为我管理一些业务数据的结构:
class Pojo
{
protected final String p;
public Pojo(String p) {
this.p = p;
}
}
我想确保p的格式是有效的,因为如果没有这种保证,这个业务对象真的没有意义;如果p是无意义的话,甚至不应该创建它。但是,验证p并非易事。
Catch
实际上它需要足够复杂的验证逻辑,逻辑应该完全可以测试它,所以我在一个单独的类中有这个逻辑:
final class BusinessLogic() implements Validator<String>
{
public String validate(String p) throws InvalidFoo, InvalidBar {
...
}
}
可能重复的问题
我的思考到目前为止
虽然以下构造函数公开声明Pojo的依赖关系并保留其简单的可测试性,但它完全不安全。这里没有任何东西阻止客户提供一个声称每个输入都可以接受的验证器!
public Pojo(Validator businessLogic, String p) throws InvalidFoo, InvalidBar {
this.p = businessLogic.validate(p);
}
所以,我稍微限制了构造函数的可见性,并且我提供了一个工厂方法,确保验证然后构造:
@VisibleForTesting
Pojo(String p) {
this.p = p;
}
public static Pojo createPojo(String p) throws InvalidFoo, InvalidBar {
Validator businessLogic = new BusinessLogic();
businessLogic.validate(p);
return new Pojo(p);
}
现在我可以将createPojo重构为工厂类,这将恢复Pojo的“单一责任”并简化工厂逻辑的测试,更不用说不再浪费地在每个新的上创建一个新的(无状态)BusinessLogic的性能优势POJO。
我的直觉是,我应该停下来,请求外界意见。我在正确的轨道上吗?
答案 0 :(得分:12)
下面的一些回应要素...... 让我知道它是否有意义/回答你的问题。
简介:我认为您的系统可以是一个简单的库,一个多层应用程序,也可以是一个复杂的分布式系统,它在验证方面实际上没有太大区别:
验证位置?
您通常希望验证输入参数:
在客户端方面,在将参数传递给服务之前,以确保早期对象在该行上有效。如果它是远程服务,或者在生成参数和创建对象之间存在复杂的流程,则尤其需要这样做。
:
Pojo
); 如何验证?
假设上述情况,由于您可能需要在多个地方重用验证逻辑,因此最好在 utility class 中提取它。这样:
更具体地说,您至少应该在构造函数中调用此逻辑,以强制实现对象的有效性(考虑将有效的依赖关系作为前置条件到Pojo
'中的算法中方法):
实用程序类:
public final class PojoValidator() {
private PojoValidator() {
// Pure utility class, do NOT instantiate.
}
public static String validateFoo(final String foo) {
// Validate the provided foo here.
// Validation logic can throw:
// - checked exceptions if you can/want to recover from an invalid foo,
// - unchecked exceptions if you consider these as runtime exceptions and can't really recover (less clutering of your API).
return foo;
}
public static Bar validateBar(final Bar bar) {
// Same as above...
// Can itself call other validators.
return bar;
}
}
Pojo类: 请注意静态导入语句以提高可读性。
import static PojoValidator.validateFoo;
import static PojoValidator.validateBar;
class Pojo {
private final String foo;
private final Bar bar;
public Pojo(final String foo, final Bar bar) {
validateFoo(foo);
validateBar(bar);
this.foo = foo;
this.bar = bar;
}
}
如何测试我的Pojo?
您应该添加创建单元测试以确保在构造时调用验证逻辑(以避免回归,因为人们稍后通过为X,Y,Z原因删除此验证逻辑来“简化”构造函数)。 p>
如果它们很简单,可以内联依赖项的创建,因为它使您的测试更具可读性,因为您使用的所有内容都是本地的(滚动较少,心理足迹较小等)
但是,如果您的Pojo依赖项的设置足够复杂/冗长以至于测试不再可读,您可以使用@Before
/ setUp
方法将此设置考虑在内,单元测试测试Pojo
的逻辑真正专注于验证Pojo的行为。
无论如何,我同意Jeff Storey的说法:
最后,将您的测试视为代码示例,示例或可执行规范:您不希望通过以下方式给出一个令人困惑的示例:
如果Pojo
需要非常复杂的依赖项,该怎么办?
[如果是这种情况,请告诉我们]
生产代码: 您可以尝试在工厂中隐藏这种复杂性。
测试代码: 之一:
编辑:在安全性的上下文中有关输入验证的一些链接,这也很有用:
答案 1 :(得分:3)
首先,我要说验证逻辑不应仅仅存在于客户端。这有助于确保您不会在数据存储中放入无效数据。如果您添加其他客户端(例如,您可能有一个胖客户端,并且您添加了一个Web服务客户端,则需要在两个位置维护验证。)
我不认为你应该有一个构造函数来构造一个没有验证的对象(使用@VisibleForTesting
注释)。您通常应该使用有效对象进行测试(除非您正在测试错误情况)。此外,在仅进行测试的生产代码中添加其他代码是代码味道,因为它不是真正的生产代码。
我认为放置验证逻辑的适当位置在域对象本身内。这样可以确保您不会创建无效的域对象。
我不太喜欢将验证器传递到域对象的想法。这为需要了解验证器的域对象的客户端提供了大量工作。如果你想创建一个单独的验证器,这可能会增加重用和模块化的好处,但我不会注入它。在测试中,您始终可以使用完全绕过验证的模拟对象。
向域模型添加验证是Web框架的常见功能(grails / rails)。我认为这是一个很好的方法,它不应该损害可测试性。
答案 2 :(得分:3)
我想确保
p
的格式有效,因为如果没有这种保证,这个业务对象真的没有意义;如果p
是无意义的话,甚至不应该创建它。但是,验证p
并非易事。
p
”表示p
的类型不仅仅是String
,而是更具体的类型。例如,EmailString
:
class Pojo
{
protected final EmailString p;
public Pojo(EmailString p) {
this.p = p;
}
}
String
和EmailString
之间必须有转换器。 这个转换器作为业务逻辑的一部分,确保p
不能无效。每当消费者拥有String
并想要创建Pojo
时,他就会使用此转换器。有几种变体如何实现此转换器以方便地捕获不可转换的情况,例如.net
样式:
class Converter
{
public static bool TryParse(String s, out EmailString p) {
}
}
可测试性对我来说非常重要,您的建议如何维护代码的这个属性?
这样,您可以与Pojo
解析/转换逻辑分开测试String
逻辑。