Cuánto daño ha hecho… IValidable (y sucedáneos)

Hay ideas que aparecen la mejor de las intenciones, que son útiles, incluso muy útiles, pero que llega un momento en que su intención original se pierde, su objetivo se diluye y su uso se deforma hasta que se convierten en malas ideas.

Ya he hablado de algunas de ellas, como el mal uso que se da ServiceLocator, pero hoy me quiero centrar en el interface IValidable. Este interface se presenta de varias formas y con diversos nombres, como IValidatableObject (muy usado en ASPNET MVC), pero en general es algo así:

public interface IValidable
{
   ValidationResult Validate();
}

public class ValidationResult
{
   private readonly IList<ValidationMessage> messages = new List<ValidationMessage>();

   public void AddError(string property, string message)
   {
       messages.Add(new ValidationMessage(property ,message);
   }

   public bool IsValid
   {
      get { return !messages.Any(); }
   }

   // ...
}

public class ValidationMessage
{
   public string PropertyName { get; private set; }
   public string ErrorMessage { get; private set; }

   public ValidationMessage(string propertyName, string errorMessage)
   {
       PropertyName = propertyName;
       ErrorMessage = errorMessage;
   }
}

La idea es simple y parece buena. Tenemos un interface que podemos implementar en aquellas clases que sean susceptibles de ser validadas. De esa forma, podremos invocar el método Validate y obtener el resultado de la validación.

Los casos de aplicación de esta técnica son numerosos y genera un código bastante limpio y manejable en muchos de ellos. Por ejemplo, mezclado con DataBinding y unos atributos de validación ([Required], [MinLength(17)], etc.) que se validan directamente desde una clase base, acaba por crear una estructura bastante apañada para lidiar con la validación en la capa de interfaz de usuario.

Esto funciona bien siempre que la clase que implementa IValidable sea una clase que esté muy restringida a un ámbito y una operación muy concretos dentro de nuestra aplicación, como puede ser un ViewModel.

También puede funcionar en aplicaciones muy simples en las que no hay casi lógica, donde mezclando Active Record y Transaction Script tienes la aplicación hecha.

¿Cuál es el problema?

El problema es que ser válido es una cosa muy relativa. ¿Es válido Sergio Ramos de central? Pues parece ser que sí. ¿Es válido Sergio Ramos como presidente de gobierno? Viendo lo que hay, podría ser. ¿Es válido Sergio Ramos como premio Nobel? Definitivamente, no. Sin embargo, el “objeto” Sergio Ramos siempre es el mismo, lo que cambia… es el contexto.

Al principio, cuando uno piensa en validación, parece que automáticamente piensa en “válido para guardar en la base de datos”, pero en cuanto una aplicación crece, aparecen más casos de validación para una misma clase, cada uno con sus propios requisitos. El interface IValidable obliga a un diseño en que un objeto es responsable de su propia validación, pero no distingue para qué se está realizando esa validación.

Otro problema es que se acaba aplicando el interface IValidable a cosas sin sentido. Me duele especialmente verlo en una entidad (en el sentido DDD) como ocurre en la aplicacón de ejemplo DDD de Microsoft España. Además de lo que acabamos de contar sobre validar para distintos contextos, ¿qué sentido tiene validar una entidad? Una entidad, y más una raíz de agregado como en ese caso, por definición, NO puede estar en un estado inválido, ya que una de sus misiones en esta vida es garantizar que los datos que contiene son coherentes y se cumplen SIEMPRE sus invariantes.

Para evitar el problema de la validación en distintos contextos, Martin Fowler propone tener un método IsValid para cada caso de validación. Por seguir con el ejemplo de Sergio Ramos, tendríamos:

public class Person
{
   public bool IsValidAsCentralDefender();
   public bool IsValidAsPresident();
   public bool IsValidAsNobelPrize();
   ...
}

Pues, aunque lo diga Martin Fowler, a mi no me convence. Creo que es una violación del OCP, del SRP y de no sé cuantas cosas más. La clase validada acaba teniendo distintas responsabilidades relativas a la validación de escenarios que, en principio, no tienen porqué ser importantes para la clase. Además, cada vez que añadimos un nuevo caso de uso, nos obliga a tocar la clase, y eso es, cuanto menos, poco deseable.

La “solución” que suelo aplicar es similar a la de Castle Validator, aunque más simple. En lugar de un interface IValidable, tengo un interface IValidator<T> con este aspecto:

public interface IValidator<T>
{
   ValidationResult Validate(T instance);
}

Así se consigue extraer la lógica de validación de la clase validada, y si hace falta añadir nuevos validadores para nuevos escenarios, no hay problema, se añaden y ya está. El código que ya existía no se toca y, por tanto, no se rompe. Las responsabilidades no se mezclan. Si hace falta reutilizar reglas de validación en distintos contextos, es fácil, se puede invocar un validador desde otro y componer el resultado.

Una ventaja adicional de hacerlo de esta manera es que la validación pasa a ser responsabilidad de servicios, que se crean a través de un contenedor de IoC y, por tanto, pueden recibir dependencias, por ejemplo de un IRepository<T> para validar que el nombre es único antes de intentar guardar en la base de datos.

Quizá te estés preguntando una cosa: si la lógica de validación está en otra clase, ¿cómo puedo validar las propiedades privadas? No puedes, pero es que no hace falta. Las propiedades privadas son privadas por algo. Son un detalle de implementación. Siempre deberían tener valores válidos, puesto que son gestionados por la clase a la que pertecenen.

En definitiva, esto es lo de siempre. No hay una solución que valga para todo, pero sí hay soluciones mejores o peores para ciertos casos. Antes de aplicar ciegamente lo que se ve por ahí (empezando por este mismo blog), es mejor pararse un poco y pensar si realmente tiene sentido.

6 comentarios en “Cuánto daño ha hecho… IValidable (y sucedáneos)

  1. José Manuel @superjmn en Twitter dijo:

    Me encanta tu forma de valorar ventajas y desventajas. Me gustaría saber qué opinas de la solución que proponen en la versión 2 beta de NLayerApp, viene como parámetro un contexto (aunque no sabría usarlo). Tu solución es bastante simple y creo que práctica. Podrías publicar un ejemplo básico, por ejemplo validando que una fecha no sea mayor que otra en una instancia de clase X?

    ¡Muchas gracias!

  2. Gracias por el comentario. El context de NLayerApp es un ValidationContext (http://msdn.microsoft.com/es-es/library/system.componentmodel.dataannotations.validationcontext.aspx), que lo veo más que nada orientado a validación para UI.

    Para validar la fecha con la idea que propongo, sería algo así como:

    public class SomePersonValidator : IValidator
    {
        public ValidationResult Validate(Person p)
        {
            var result = new ValidationResult();
            if (p.DeathDate < p.BirthDate)
                result.AddMessage("DeathDate", "Nadie se muere antes de nacer");
            return result;
        }
    }
    
  3. José Manuel @superjmn en Twitter dijo:

    Impresionante. Gracias, Juanma!
    (Por cierto, tengo tu blog en mi página de inicio).

  4. Gran post! Una pregunta, como implementarias el ejemplo del post con el tipo Person? Necesitarias tres clases para implementar tres validaciones? Algo asi como
    public class CentralDefenderValidator : IValidator
    public class PresidentValidator : IValidator
    public class NobelPrizeValidator : IValidator?

  5. Sí, lo haría con tres clases. Podrías llegar a mezclarlo todo en una usando interfaces marcadoras e implementación explícita de interfaces, pero no le veo mucha ventaja.

    Además es probable que cada validador tenga dependencias específicas, por lo que separarlos en clases parece razonable.

  6. Como todo en el mundo del desarrollo, las herramientas son útiles siempre que se utilicen como se deben. La validación con DataAnnotations es super útil, siempre y cuando se entienda su propósito.

    Y lo mismo pasa con ServiceLocator. A veces se llega a exagerar al demonizar ciertos conceptos, he leído cosas como que el SL es un antipattern, y al final de cuentas TODO puede terminar siendo un antipattern si se da mal uso.

Comentarios cerrados.