Domain Validation in a CQRS architecture

前端 未结 11 1533
南旧
南旧 2020-12-12 11:25

Danger ... Danger Dr. Smith... Philosophical post ahead

The purpose of this post is to determine if placing the validation logic outside of my domain entiti

11条回答
  •  旧时难觅i
    2020-12-12 11:32

    The validation in your example is validation of a value object, not an entity (or aggregate root).

    I would separate the validation into distinct areas.

    1. Validate internal characteristics of the Email value object internally.

    I adhere to the rule that aggregates should never be in an invalid state. I extend this principal to value objects where practical.

    Use createNew() to instantiate an email from user input. This forces it to be valid according to your current rules (the "user@email.com" format, for example).

    Use createExisting() to instantiate an email from persistent storage. This performs no validation, which is important - you don't want an exception to be thrown for a stored email that was valid yesterday but invalid today.

    class Email
    {
        private String value_;
    
        // Error codes
        const Error E_LENGTH = "An email address must be at least 3 characters long.";
        const Error E_FORMAT = "An email address must be in the 'user@email.com' format.";
    
        // Private constructor, forcing the use of factory functions
        private Email(String value)
        {
            this.value_ = value;
        }
    
        // Factory functions
        static public Email createNew(String value)
        {
            validateLength(value, E_LENGTH);
            validateFormat(value, E_FORMAT);
        }
    
        static public Email createExisting(String value)
        {
            return new Email(value);
        }
    
        // Static validation methods
        static public void validateLength(String value, Error error = E_LENGTH)
        {
            if (value.length() < 3)
            {
                throw new DomainException(error);
            }
        }
    
        static public void validateFormat(String value, Error error = E_FORMAT)
        {
            if (/* regular expression fails */)
            {
                throw new DomainException(error);
            }
        }
    
    }
    
    1. Validate "external" characteristics of the Email value object externally, e.g., in a service.

      class EmailDnsValidator implements IEmailValidator
      {
          const E_MX_MISSING = "The domain of your email address does not have an MX record.";
      
          private DnsProvider dnsProvider_;
      
          EmailDnsValidator(DnsProvider dnsProvider)
          {
              dnsProvider_ = dnsProvider;
          }
      
          public void validate(String value, Error error = E_MX_MISSING)
          {
              if (!dnsProvider_.hasMxRecord(/* domain part of email address */))
              {
                  throw new DomainException(error);
              }
          }
      }
      
      class EmailDomainBlacklistValidator implements IEmailValidator
      {
          const Error E_DOMAIN_FORBIDDEN = "The domain of your email address is blacklisted.";
      
          public void validate(String value, Error error = E_DOMAIN_FORBIDDEN)
          {
              if (/* domain of value is on the blacklist */))
              {
                  throw new DomainException(error);
              }
          }
      }
      

    Advantages:

    • Use of the createNew() and createExisting() factory functions allow control over internal validation.

    • It is possible to "opt out" of certain validation routines, e.g., skip the length check, using the validation methods directly.

    • It is also possible to "opt out" of external validation (DNS MX records and domain blacklisting). E.g., a project I worked on initially validated the existance of MX records for a domain, but eventually removed this because of the number of customers using "dynamic IP" type solutions.

    • It is easy to query your persistent store for email addresses that do not fit the current validation rules, but running a simple query and treating each email as "new" rather than "existing" - if an exception is thrown, there's a problem. From there you can issue, for example, a FlagCustomerAsHavingABadEmail command, using the exception error message as guidance for the user when they see the message.

    • Allowing the programmer to supply the error code provides flexibility. For example, when sending a UpdateEmailAddress command, the error of "Your email address must be at least 3 characters long" is self explanatory. However, when updating multiple email addresses (home and work), the above error message does not indicate WHICH email was wrong. Supplying the error code/message allows you to provide richer feedback to the end user.

提交回复
热议问题