Patrones de diseño en DDD: Validación en las entidades

viernes, 14 de febrero de 2014


Unos de los patrones más habituales en DDD son las entidades, ya escribí un post sobre este patrón. Ahora me quiero centrar más en la validación de la entidades y donde debería estar esta lógica, es una decisión que tarde o temprano se tiene que tomar.

En principio lo mas sencillo es que la propia entidad contenga la lógica de validación. Vamos a ver un ejemplo donde necesitamos validar que un usuario tiene los campos mínimos rellenos para poder registrarse en una web. Añadimos un método IsValid a la entidad User que comprueba que las propiedades obligatorias tienen valor.
public class User
    {
        public int UserId { get; set; }
        public string UserName { get; set; }
        public string Password { get; set; }
        public string Email { get; set; }

        public bool IsValid()
        {return BrokenRules().Count() > 0;}

        public IEnumerable<string> BrokenRules()
        {
            if (string.IsNullOrEmpty(UserName))
                yield return "Must include a username";
            if (string.IsNullOrEmpty(Email))
                yield return "Must include a email";
            if (string.IsNullOrEmpty(Password))
                yield return "Must include a password";

            yield break;
        }
    }

De esta forma queda el código simple, la lógica esta encapsulada porque la entidad contiene toda la información necesaria para poder validar. En validaciones sencillas esta es la solución más adecuada, sin embargo en una aplicación empresarial lo normal es que las validaciones cambien más a menudo que la propia entidad y empiecen a entrar en juego contextos de validación por lo que tenemos que adoptar otro tipo de soluciones ya que la entidad no va a disponer de toda la información para poder validar.
Siguiendo con el mismo ejemplo del usuario, donde se debía validar un usuario en un registro por email teniendo unos campos requeridos. Ahora complicamos el ejemplo y le añadimos varios contextos como puede ser que aparte de registro por email, queremos incluir en la web registro mediante Facebook y Twitter también. Ahora con esta nueva funcionalidad debemos cambiar el código de validación, lo primero que podemos pensar es en añadir un parámetro al método IsValid que indique que tipo de validación es requerida, el problema de esta solución es que si en el futuro se añaden más validaciones como puede ser registro mediante google+ vamos a tener que tocar siempre sobre el mismo código y corremos el riesgo de estropear validaciones que ya funcionan bien y esto no es muy escalable. Una solución más limpia puede ser usar el patrón de diseño Visitor.
Siguiendo este patrón vamos a crearnos una clase de validación para cada contexto, es decir, una clase de validación para el registro mediante email y otro para el registro mediante Facebook, implementando ambas clases la misma interface.
public interface IValidator<T>
    {
        bool IsValid(T entity, ref IEnumerable<string> brokenRules);
    }

public class EmailRegisterValidator : IValidator
    {
        public bool IsValid(User user, ref IEnumerable brokenRules)
        {
            brokenRules = BrokenRules(user);

            return brokenRules.Count() > 0;
        }

        private IEnumerable BrokenRules(User user)
        {
            if (string.IsNullOrEmpty(UserName))
                yield return "Must include a username";
            if (string.IsNullOrEmpty(Email))
                yield return "Must include a email";
            if (string.IsNullOrEmpty(Password))
                yield return "Must include a password";

            yield break;
        }
    }

public class FacebookRegisterValidator : IValidator<User>
    {
        public bool IsValid(User user, ref IEnumerable<string> brokenRules)
        {
            brokenRules = BrokenRules(user);

            return brokenRules.Count() > 0;
        }

        private IEnumerable<string> BrokenRules(User user)
        {
            if (string.IsNullOrEmpty(UserName))
                yield return "Must include a username";
            if (string.IsNullOrEmpty(Email))
                yield return "Must include a email";
            if (string.IsNullOrEmpty(Password))
                yield return "Must include a password";
            if (string.IsNullOrEmpty(user.UserIdFacebook))
                yield return "Must include a UserIdFacebook";

            yield break;
        }
    }

Y ahora modificamos la clase user para que reciba el validador en el método validate y podríamos devolver mediante un parámetro por referencia los errores encontrados que devuelve el validador por ejemplo, aunque en una aplicación real entraría en juego el control de excepciones aplicado para este tema.
   public class User
    {
        public int UserId { get; set; }
        public string UserName { get; set; }
        public string Password { get; set; }
        public string Email { get; set; }

        public string UserIdFacebook { get; set; }

        public bool Validate(IValidator<User> validator, ref IEnumerable<string> brokenRules)
        {
            return validator.IsValid(this, ref brokenRules);
        }
    }

De esta forma si en el futuro añadimos una nueva validación, crearíamos una clase validador nueva y se la pasaríamos cuando corresponda a la entidad. De esta forma no modificamos la entidad User con cada nueva validación y esta solución es más escalable.

Libros Relacionados

Domain-Driven Design: Tackling Complexity in the Heart of Software

Implementing Domain-Driven Design

No hay comentarios:

Publicar un comentario