13

问题

在需要复杂验证逻辑的情况下,我如何才能最好地管理对象图的构造?出于可测试性的原因,我想保留依赖注入的、无操作的构造函数。

可测试性对我来说非常重要,您对维护代码的这个属性有何建议?

背景

我有一个普通的旧 java 对象,它为我管理一些业务数据的结构:

class Pojo
{
    protected final String p;

    public Pojo(String p) {
        this.p = p;
    }
}

我想确保 p 的格式有效,因为如果没有这种保证,这个业务对象真的毫无意义;如果 p 是无意义的,甚至不应该创建它。但是,验证 p 并非易事。

抓住

真的,它需要足够复杂的验证逻辑,该逻辑本身应该是完全可测试的,所以我在一个单独的类中拥有该逻辑:

final class BusinessLogic() implements Validator<String>
{
    public String validate(String p) throws InvalidFoo, InvalidBar {
        ...
    }
}

可能的重复问题

  • 验证逻辑应该在哪里实现?- 接受的答案对我来说是不可理解的。我将“在班级的本地环境中运行”读为重言式,验证规则如何在“班级的本地环境”之外的任何地方运行?第 2 点我无法理解,所以我无法发表评论。
  • 在哪里提供验证的逻辑规则?- 两个答案都表明责任在于客户/数据提供者,我原则上喜欢这一点。但是,在我的情况下,客户端可能不是数据的发起者并且无法验证它。
  • 在哪里保留验证逻辑?- 建议模型可以拥有验证,但是我发现这种方法不太适合测试。具体来说,对于每个单元测试,即使我正在测试模型的其他部分,我也需要关心所有的验证逻辑——按照建议的方法,我永远无法完全隔离我想要测试的内容。

到目前为止我的想法

尽管下面的构造函数公开声明了 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 的“单一职责”并简化工厂逻辑的测试,更不用说不再浪费在每个新 Pojo 上创建一个新的(无状态的)BusinessLogic 的性能优势。

我的直觉是我应该停下来,征求外界的意见。我在正确的轨道上吗?

4

3 回答 3

13

下面的一些回应元素......让我知道它是否有意义/回答你的问题。


简介:我认为您的系统可以是简单的库、多层应用程序或复杂的分布式系统,在验证方面实际上并没有太大区别:

  • 客户端:远程客户端(例如 HTTP 客户端)或只是另一个调用您的库的类。
  • 服务:远程服务(例如 REST 服务)或您公开的 API。

在哪里验证?

您通常希望验证您的输入参数:

  • 客户端,在将参数传递给服务之前,确保尽早确保对象将有效。如果它是一个远程服务或者在参数的生成和对象创建之间存在复杂的流程,这尤其可取。

  • 服务方面:

    • 在类级别,在您的构造函数中,以确保您创建有效的对象;
    • 在子系统级别,即管理这些对象的层(例如 DAL 持久化您Pojo的 s);
    • 在服务的边界,例如库或控制器的外观或外部 API(如在 MVC 中,例如 REST 端点、Spring 控制器等)。

如何验证?

假设上述情况,由于您可能必须在多个地方重用验证逻辑,因此将其提取到实用程序类中可能是个好主意。这边走:

  • 你不要复制它(干!);
  • 您确信系统的每一层都会以相同的方式进行验证;
  • 您可以轻松地单独对该逻辑进行单元测试(因为它是无状态的)。

更具体地说,您至少会在构造函数中调用此逻辑,以强制对象的有效性(考虑将有效的依赖关系作为算法的先决条件存在于Pojo's 的方法中):

实用程序类

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?

  1. 您应该添加创建单元测试以确保在构造时调用您的验证逻辑(以避免回归,因为人们稍后通过删除此验证逻辑来​​“简化”您的构造函数,原因是 X、Y、Z)。

  2. 如果依赖项很简单,则内联创建依赖项,因为它使您的测试更具可读性,因为您使用的所有内容都是本地的(更少的滚动,更小的心理足迹等)

  3. 但是,如果您的 Pojo 依赖项的设置足够复杂/冗长以至于测试不再可读,您可以将此设置分解为@Before/setUp方法,以便单元测试测试Pojo的逻辑真正专注于验证您的行为波乔。

无论如何,我同意 Jeff Storey:

  1. 使用有效参数编写测试,
  2. 没有没有验证的构造函数只是为了你的测试当您混合生产测试代码时,这确实是一种代码味道,并且肯定会在某些时候被某人无意中使用,包括您的服务的稳定性。

最后,将您的测试视为代码示例、示例或可执行规范:您不想通过以下方式给出一个令人困惑的示例:

  • 注入无效参数;
  • 注入验证器,这会使您的 API 混乱/读取“奇怪”。

如果Pojo需要非常复杂的依赖项怎么办?

[如果是这种情况,请告诉我们]

生产代码:您可以尝试在工厂中隐藏这种复杂性。

测试代码:要么:

  • 如上所述,以不同的方法将其排除在外;或者
  • 使用你的工厂;或者
  • 使用模拟参数,并对其进行配置,以便它们验证测试通过的要求。

编辑:一些关于安全上下文中输入验证的链接,这也很有用:

于 2013-03-11T12:01:32.363 回答
3

首先,我想说验证逻辑不应该只存在于客户端中。这有助于确保您最终不会将无效数据放入数据存储中。如果您添加其他客户端(例如,您可能有一个胖客户端并添加了一个 Web 服务客户端,您需要在这两个地方维护验证)。

我认为您不应该有一个构造函数来构造一个不验证的对象(使用该@VisibleForTesting注释)。您通常应该使用有效对象进行测试(除非您正在测试错误案例)。此外,在生产代码中添加用于测试的附加代码是一种代码味道,因为它不是真正的生产代码。

我认为放置验证逻辑的适当位置是域对象本身。这将确保您不会创建无效的域对象。

我不太喜欢将验证器传递给域对象的想法。这给需要了解验证器的域对象客户带来了很多工作。如果您想创建一个单独的验证器,这可能会增加重用和模块化的好处,但我不会注入它。在测试中,您始终可以使用完全绕过验证的模拟对象。

向域模型添加验证是 Web 框架(grails/rails)常见的事情。我认为这是一个很好的方法,它不应该损害可测试性。

于 2013-03-10T22:25:37.357 回答
3

我想确保它p的格式有效,因为如果没有这种保证,这个业务对象真的毫无意义;p如果是胡说八道,甚至不应该创建它。但是,验证p并非易事。

  1. p有效格式”意味着类型p不仅仅是String,而是一些更具体的类型。

例如EmailString

class Pojo
{
    protected final EmailString p;

    public Pojo(EmailString p) {
        this.p = p;
    }
}
  1. String和之间必须有一个转换器EmailString这个转换器,作为业务逻辑的一部分,确保p不会无效。每当消费者拥有String并想要创建Pojo时,他都会使用此转换器。

有几种变体如何实现此转换器以方便地捕获不可转换的情况,例如,.net样式:

class Converter
{
    public static bool TryParse(String s, out EmailString p) {

    }
}

可测试性对我来说非常重要,您对维护代码的这个属性有何建议?

这样您就可以将逻辑与解析/转换逻辑Pojo分开测试。String

于 2015-05-25T13:04:10.480 回答