验证逻辑应该在哪里生效?

时间:2013-03-10 22:06:11

标签: java oop validation constructor

问题

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

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

背景

我有一个普通的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 {
        ...
    }
}

可能重复的问题

  • Where Should Validation Logic Be Implemented? - 接受的答案对我来说是难以理解的。我读作“在班级的本土环境中运行”作为重言式,如何在“班级的本地环境”以外的任何其他方面运行验证规则?第2点我没有理解,所以我无法发表评论。
  • Where To Provide Logic Rules for Validation? - 两个答案都表明责任在于客户/数据提供者,我原则上喜欢它。但是,在我的情况下,客户端可能不是数据的发起者,无法验证它。
  • Where To Keep Validation Logic? - 建议验证可以归模型所有,但我觉得这种方法不太适合测试。具体来说,对于每个单元测试,我需要关心所有验证逻辑,即使在我测试模型的其他部分时 - 我也无法通过遵循建议的方法完全隔离我想要测试的内容。

我的思考到目前为止

虽然以下构造函数公开声明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。

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

3 个答案:

答案 0 :(得分:12)

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


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

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

验证位置?

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

  • 客户端方面,在将参数传递给服务之前,以确保早期对象在该行上有效。如果它是远程服务,或者在生成参数和创建对象之间存在复杂的流程,则尤其需要这样做。

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

如何验证?

假设上述情况,由于您可能需要在多个地方重用验证逻辑,因此最好在 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?

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

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

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

  4. 无论如何,我同意Jeff Storey的说法:

    1. 使用有效参数编写测试
    2. 没有没有验证的构造函数只是为了您的测试。当你混合生产测试代码时,它确实是代码味道,并且肯定会被某人在某个时刻无意中使用,包括服务的稳定性。
    3. 最后,将您的测试视为代码示例,示例或可执行规范:您不希望通过以下方式给出一个令人困惑的示例:

      • 注入无效参数;
      • 注入验证器,这会弄乱你的API /读取“怪异”。

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

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

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

      测试代码: 之一:

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

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

答案 1 :(得分:3)

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

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

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

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

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

答案 2 :(得分:3)

  

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

  1. “有效格式的p”表示p的类型不仅仅是String,而是更具体的类型。
  2. 例如,EmailString

    class Pojo
    {
        protected final EmailString p;
    
        public Pojo(EmailString p) {
            this.p = p;
        }
    }
    
    1. StringEmailString之间必须有转换器。 这个转换器作为业务逻辑的一部分,确保p不能无效。每当消费者拥有String并想要创建Pojo时,他就会使用此转换器。
    2. 有几种变体如何实现此转换器以方便地捕获不可转换的情况,例如.net样式:

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

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

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