验证不可变值对象的良好实践

时间:2012-03-19 18:58:38

标签: java domain-driven-design validation

假设MailConfiguration类指定发送邮件的设置:

public class MailConfiguration {

  private AddressesPart addressesPart;

  private String subject;

  private FilesAttachments filesAttachments;

  private String bodyPart;

  public MailConfiguration(AddressesPart addressesPart, String subject, FilesAttachments filesAttachements,
    String bodyPart) {
    Validate.notNull(addressesPart, "addressesPart must not be null");
    Validate.notNull(subject, "subject must not be null");
    Validate.notNull(filesAttachments, "filesAttachments must not be null");
    Validate.notNull(bodyPart, "bodyPart must not be null");
    this.addressesPart = addressesPart;
    this.subject = subject;
    this.filesAttachements = filesAttachements;
    this.bodyPart = bodyPart;
  }
  // ...  some useful getters ......

}

所以,我正在使用两个值对象:AddressesPart和FilesAttachment。

这两个值对象具有相似的结构,所以我只在这里公开AddressesPart:

public class AddressesPart {

  private final String senderAddress;

  private final Set recipientToMailAddresses;

  private final Set recipientCCMailAdresses;

  public AddressesPart(String senderAddress, Set recipientToMailAddresses, Set recipientCCMailAdresses) {
    validate(senderAddress, recipientToMailAddresses, recipientCCMailAdresses);
    this.senderAddress = senderAddress;
    this.recipientToMailAddresses = recipientToMailAddresses;
    this.recipientCCMailAdresses = recipientCCMailAdresses;
  }

  private void validate(String senderAddress, Set recipientToMailAddresses, Set recipientCCMailAdresses) {
    AddressValidator addressValidator = new AddressValidator();
    addressValidator.validate(senderAddress);
    addressValidator.validate(recipientToMailAddresses);
    addressValidator.validate(recipientCCMailAdresses);
  }

  public String getSenderAddress() {
    return senderAddress;
  }

  public Set getRecipientToMailAddresses() {
    return recipientToMailAddresses;
  }

  public Set getRecipientCCMailAdresses() {
    return recipientCCMailAdresses;
  }

}

以及相关的验证程序:AddressValidator

public class AddressValidator {

  private static final String EMAIL_PATTERN
    = "^[_A-Za-z0-9-]+(\\.[_A-Za-z0-9-]+)*@[A-Za-z0-9]+(\\.[A-Za-z0-9]+)*(\\.[A-Za-z]{2,})$";

  public void validate(String address) {
    validate(Collections.singleton(address));
  }

  public void validate(Set addresses) {
    Validate.notNull(addresses, "List of mail addresses must not be null");
    for (Iterator it = addresses.iterator(); it.hasNext(); ) {
      String address = (String) it.next();
      Validate.isTrue(address != null && isAddressWellFormed(address), "Invalid Mail address " + address);
    }
  }

  private boolean isAddressWellFormed(String address) {
    Pattern emailPattern = Pattern.compile(EMAIL_PATTERN);
    Matcher matcher = emailPattern.matcher(address);
    return matcher.matches();
  }
}

因此,我有两个问题:

1)如果由于某些原因,稍后我们想要以不同方式验证地址邮件(例如包含/排除与现有mailingList匹配的一些别名),我应该将一种IValidator暴露为构造函数参数吗?喜欢以下而不是带来具体的依赖(就像我做的那样):

public AddressValidator(IValidator myValidator) {
   this.validator = myValidator;
}

实际上,这将遵循SOLID原则的D原则:依赖注入。

但是,如果我们遵循这个逻辑,那么大多数的价值对象会拥有一个抽象的验证器吗?或者大部分时间它只是一种矫枉过正(想到YAGNI?)?

2)我读过一些文章而不是DDD,所有的验证都必须存在并且只出现在Aggregate Root中,在这种情况下意味着:MailConfiguration。

如果我认为不可变对象永远不应处于不粘合状态,我是否正确?因此,在构造函数中验证是否会在相关实体中被优先考虑(因此避免聚合担心验证它的“子”)?

4 个答案:

答案 0 :(得分:4)

DDD中有一个基本模式可以完美地完成检查和组装对象以创建新对象的工作:Factory

  

我读过一些文章而不是关于DDD的所有验证   必须存在且仅存在于聚合根

我非常不同意这一点。在DDD的各个地方都可以有验证逻辑:

  • 创建时的验证,由工厂执行
  • 执行聚合不变量,通常在聚合根
  • 中完成
  • 可以在域服务中找到跨越多个对象的验证。

另外,我觉得很有趣的是你打扰创建一个AddressesPart值对象 - 这是一件好事,而不考虑首先将EMailAddress作为一个值对象。我认为它使你的代码变得复杂很多,因为没有封装的电子邮件地址概念,因此AddressesPart(以及任何将操纵地址的对象)被迫处理AddressValidator以执行其地址的验证。我认为它不应该是它的责任,而是AddressFactory的责任。

答案 1 :(得分:2)

我不太确定我是否100%关注你,但是只有在有效的情况下才允许创建不可变对象的一种方法是使用Essence Pattern

简而言之,我们的想法是父类包含一个静态工厂,它根据内部“本质”类的实例创建自身的不可变实例。内在的本质是可变的,允许对象建立起来,所以你可以随时把各个部分放在一起,也可以在整个过程中进行验证。

SOLID主体和良好的DDD遵循,因为父不可变类仍然只做一件事,但允许其他人通过它的“本质”来构建它。

有关此示例,请查看Ldap extension到Spring Security库。

答案 2 :(得分:1)

首先是一些观察结果。

为什么没有泛型? J2SE5.0于2004年问世。

当前版本的Java SE标准为Objects.requiresNonNull。一点点,大写是错误的。还返回传递的对象,因此不需要单独的行。

    this.senderAddress = requiresNonNull(senderAddress);

你的课程不是一成不变的。它们是可子类化的。他们也没有安全地复制他们的可变参数(Set s - 遗憾的是,Java库中还没有不可变的集合类型)。注意,在验证前复制。

    this.recipientToMailAddresses = validate(new HashSet<String>(
        recipientToMailAddresses
    ));

在正则表达式中使用^$有点误导。

如果验证有所不同,那么有两个明显的(理智的)选择:

  • 只做这个班级中最广泛的变化。在将要使用的上下文中更具体地进行验证。
  • 传入使用的验证器并将其作为属性。为了有用,客户端代码必须检查并使用此信息做一些合理的事情,这是不可能的。

将验证器传递给构造函数然后丢弃它并没有多大意义。这使得构造函数过于复杂。如果必须,请将其置于静态方法中。

封闭实例应检查其参数是否对该特定用途有效,但不应与类重叠,以确保它们通常有效。它会在哪里结束?

答案 3 :(得分:0)

虽然这是一个老问题,但对于任何绊倒主题的人来说,请使用POJO(Plain Old Java Objects)保持简单。 至于验证,没有单一的事实,因为对于纯DDD,您需要始终牢记上下文。 例如,可以并且应该允许没有信用卡数据的用户创建帐户。但是,在一些购物篮页面上查看时需要信用卡数据。 如何通过DDD精心解决这个问题的方法是将代码中的一些代码移动到它自然所属的实体和值对象中。

作为第二个示例,如果地址在域级别任务的上下文中永远不应为空,则地址值对象应强制在对象内部进行此断言,而不是使用要求第三方库检查某个值对象是否为是否为空。

此外,与ShippingAddress,HomeAddress或CurrentResidentialAddress相比,Address作为一个独立的价值对象并没有多少传递......无处不在的语言,换句话说,名称传达了他们的意图。