Dónde poner la validación de reglas globales en DDD


Soy nuevo en DDD, y estoy tratando de aplicarlo en la vida real. No hay preguntas sobre esta lógica de validación, como null check, empty strings check, etc - que va directamente a entity constructor / property. Pero, ¿dónde poner la validación de algunas reglas globales como 'Nombre de usuario único'?

Por lo tanto, tenemos entidad Usuario

public class User : IAggregateRoot
{
   private string _name;

   public string Name
   {
      get { return _name; }
      set { _name = value; }
   }

   // other data and behavior
}

Y repositorio para usuarios

public interface IUserRepository : IRepository<User>
{
   User FindByName(string name);
}

Las opciones son:

  1. Inyectar repositorio a la entidad
  2. Inyectar repositorio a fábrica
  3. Crear operación en el servicio de dominio
  4. ???

Y cada opción más detallada:

1 .Inyectar repositorio a la entidad

Puedo consultar el repositorio en entities constructor/property. Pero creo que mantener la referencia al repositorio en entidad es un mal olor.

public User(IUserRepository repository)
{
    _repository = repository;
}

public string Name
{
    get { return _name; }
    set 
    {
       if (_repository.FindByName(value) != null)
          throw new UserAlreadyExistsException();

       _name = value; 
    }
}

Actualización: Podemos usar DI para ocultar la dependencia entre User y IUserRepository a través de Specification object.

2. Inyectar repositorio a fábrica

Puedo poner esta lógica de verificación en UserFactory. Pero, ¿qué pasa si queremos cambiar el nombre del usuario ya existente?

3. Crear operación en el servicio de dominio

Puedo crear un servicio de dominio para crear y editar usuarios. Pero alguien puede editar directamente el nombre del usuario sin llamar a ese servicio...

public class AdministrationService
{
    private IUserRepository _userRepository;

    public AdministrationService(IUserRepository userRepository)
    {
        _userRepository = userRepository;
    }

    public void RenameUser(string oldName, string newName)
    {
        if (_userRepository.FindByName(newName) != null)
            throw new UserAlreadyExistException();

        User user = _userRepository.FindByName(oldName);
        user.Name = newName;
        _userRepository.Save(user);
    }
}

4. ???

¿Dónde pone la lógica de validación global para las entidades?

Gracias!

Author: Sergey Berezovskiy, 0000-00-00

9 answers

La mayoría de las veces es mejor colocar este tipo de reglas en objetos Specification. Puede colocar estos Specificationen sus paquetes de dominio, para que cualquiera que use su paquete de dominio tenga acceso a ellos. Usando una especificación, puede agrupar sus reglas de negocio con sus entidades, sin crear entidades difíciles de leer con dependencias no deseadas en servicios y repositorios. Si es necesario, puede inyectar dependencias en servicios o repositorios en una especificación.

Dependiendo del contexto, puede crear diferentes validadores utilizando los objetos de especificación.

La principal preocupación de las entidades debe ser realizar un seguimiento del estado del negocio, ya es suficiente responsabilidad y no deben preocuparse por la validación.

Ejemplo

public class User
{
    public string Id { get; set; }
    public string Name { get; set; }
}

Dos especificaciones:

public class IdNotEmptySpecification : ISpecification<User>
{
    public bool IsSatisfiedBy(User subject)
    {
        return !string.IsNullOrEmpty(subject.Id);
    }
}


public class NameNotTakenSpecification : ISpecification<User>
{
    // omitted code to set service; better use DI
    private Service.IUserNameService UserNameService { get; set; } 

    public bool IsSatisfiedBy(User subject)
    {
        return UserNameService.NameIsAvailable(subject.Name);
    }
}

Y un validador:

public class UserPersistenceValidator : IValidator<User>
{
    private readonly IList<ISpecification<User>> Rules =
        new List<ISpecification<User>>
            {
                new IdNotEmptySpecification(),
                new NameNotEmptySpecification(),
                new NameNotTakenSpecification()
                // and more ... better use DI to fill this list
            };

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

    public IEnumerable<string> BrokenRules(User entity)
    {
        return Rules.Where(rule => !rule.IsSatisfiedBy(entity))
                    .Select(rule => GetMessageForBrokenRule(rule));
    }

    // ...
}

Para completar, las interfaces:

public interface IValidator<T>
{
    bool IsValid(T entity);
    IEnumerable<string> BrokenRules(T entity);
}

public interface ISpecification<T>
{
    bool IsSatisfiedBy(T subject);
}

Notas

Creo que la respuesta anterior de Vijay Patel va en la dirección correcta, pero siento que está un poco fuera. Sugiere que la entidad de usuario depende de la especificación, donde creo que esto debería ser al revés. De esta manera, puede dejar que la especificación dependa de servicios, repositorios y contexto en general, sin hacer que su entidad dependa de ellos a través de una dependencia de especificación.

Referencias

Una pregunta relacionada con una buena respuesta con un ejemplo: Validación en un Diseño impulsado por Dominio.

[6] Eric Evans describe el uso del patrón de especificación para validación, selección y construcción de objetos en capítulo 9, pp 145.

Este artículo sobre el patrón de especificación con una aplicación en.Net podría ser de su interés.

 52
Author: Marijn,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 12:25:29

No recomendaría no permitir cambiar propiedades en entity, si es una entrada de usuario. Por ejemplo, si la validación no se aprobó, aún puede usar la instancia para mostrarla en la interfaz de usuario con los resultados de validación, lo que permite al usuario corregir el error.

Jimmy Nilsson en su "Applying Domain-Driven Design and Patterns" recomienda validar para una operación en particular, no solo para persistir. Mientras que una entidad podría persistir con éxito, la validación real se produce cuando un la entidad está a punto de cambiar su estado, por ejemplo, el estado 'Ordenado' cambia a'Comprado'.

Al crear, la instancia debe ser válida para guardar, lo que implica verificar la unicidad. Es diferente de valid-for-ordering, donde no solo se debe verificar la singularidad, sino también, por ejemplo, la credibilidad de un cliente y la disponibilidad en la tienda.

Por lo tanto, la lógica de validación no debe invocarse en asignaciones de propiedades, debe invocarse en operaciones de nivel agregado, si son persistentes o no.

 11
Author: George Polevoy,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-04-28 18:41:38

Editar: A juzgar por las otras respuestas, el nombre correcto para tal 'servicio de dominio' es especificación. He actualizado mi respuesta para reflejar esto, incluyendo un ejemplo de código más detallado.

Yo iría con la opción 3; crear una especificación domain service que encapsula la lógica real que realiza la validación. Por ejemplo, la especificación llama inicialmente a un repositorio, pero puede reemplazarlo con una llamada a un servicio web en una etapa posterior. Tener todo esa lógica detrás de una especificación abstracta mantendrá el diseño general más flexible.

Para evitar que alguien edite el nombre sin validarlo, haga que la especificación sea un aspecto obligatorio de la edición del nombre. Puede lograr esto cambiando la API de su entidad a algo como esto:

public class User
{
    public string Name { get; private set; }

    public void SetName(string name, ISpecification<User, string> specification)
    {
        // Insert basic null validation here.

        if (!specification.IsSatisfiedBy(this, name))
        {
            // Throw some validation exception.
        }

        this.Name = name;
    }
}

public interface ISpecification<TType, TValue>
{
    bool IsSatisfiedBy(TType obj, TValue value);
}

public class UniqueUserNameSpecification : ISpecification<User, string>
{
    private IUserRepository repository;

    public UniqueUserNameSpecification(IUserRepository repository)
    {
        this.repository = repository;
    }

    public bool IsSatisfiedBy(User obj, string value)
    {
        if (value == obj.Name)
        {
            return true;
        }

        // Use this.repository for further validation of the name.
    }
}

Su código de llamada se vería algo como esto:

var userRepository = IoC.Resolve<IUserRepository>();
var specification = new UniqueUserNameSpecification(userRepository);

user.SetName("John", specification);

Y, por supuesto, puede burlarse de ISpecification en sus pruebas unitarias para facilitar las pruebas.

 7
Author: Niels van der Rest,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-04-30 09:47:51

Usaría una Especificación para encapsular la regla. Luego puede llamar cuando se actualice la propiedad UserName (o desde cualquier otro lugar que pueda necesitarla):

public class UniqueUserNameSpecification : ISpecification
{
  public bool IsSatisifiedBy(User user)
  {
     // Check if the username is unique here
  }
}

public class User
{
   string _Name;
   UniqueUserNameSpecification _UniqueUserNameSpecification;  // You decide how this is injected 

   public string Name
   {
      get { return _Name; }
      set
      {
        if (_UniqueUserNameSpecification.IsSatisifiedBy(this))
        {
           _Name = value;
        }
        else
        {
           // Execute your custom warning here
        }
      }
   }
}

No importará si otro desarrollador intenta modificar User.Name directamente, porque la regla siempre se ejecutará.

Más información aquí

 3
Author: Vijay Patel,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-04-29 08:22:29

No soy un experto en DDD pero me he hecho las mismas preguntas y esto es lo que se me ocurrió: La lógica de validación debería ir normalmente al constructor / factory y a los setters. De esta manera garantiza que siempre tiene objetos de dominio válidos. Pero si la validación implica consultas de base de datos que afectan su rendimiento, una implementación eficiente requiere un diseño diferente.

(1) Inyectar entidades: Inyectar entidades puede ser técnico difícil y también hace administrar el rendimiento de las aplicaciones muy difícil debido a la fragmentación de la lógica de la base de datos. Las operaciones aparentemente simples ahora pueden tener un impacto inesperado en el rendimiento. También hace imposible optimizar su objeto de dominio para operaciones en grupos del mismo tipo de entidades, ya no puede escribir una sola consulta de grupo y, en su lugar, siempre tiene consultas individuales para cada entidad.

(2) Inyectando repositorio: No debe poner ninguna lógica de negocio en los repositorios. Mantener repositorios simples y enfocados. Deben actuar como si fueran colecciones y solo contienen lógica para agregar, eliminar y encontrar objetos (algunos incluso derivan los métodos find a otros objetos).

(3) Domain service Este parece el lugar más lógico para manejar la validación que requiere la consulta de la base de datos. Una buena implementación haría que el paquete constructor/factory y setters involucrados sean privados, de modo que las entidades solo se pueden crear / modificar con el dominio Servicio.

 2
Author: Kdeveloper,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-04-28 18:59:39

En mi Framework CQRS, cada clase de Manejador de comandos también contiene un método ValidateCommand, que luego llama a la lógica de negocio/validación apropiada en el Dominio (implementada principalmente como métodos de Entidad o métodos estáticos de Entidad).

Así que la persona que llama haría lo siguiente:

if (cmdService.ValidateCommand(myCommand) == ValidationResult.OK)
{
    // Now we can assume there will be no business reason to reject
    // the command
    cmdService.ExecuteCommand(myCommand); // Async
}

Cada Manejador de Comandos especializado contiene la lógica de envoltura, por ejemplo:

public ValidationResult ValidateCommand(MakeCustomerGold command)
{
    var result = new ValidationResult();
    if (Customer.CanMakeGold(command.CustomerId))
    {
        // "OK" logic here
    } else {
        // "Not OK" logic here
    }
}

El método ExecuteCommand del controlador de comandos llamará a ValidateCommand () de nuevo, así que incluso si el cliente no se molestó, no pasará nada en el Dominio que no se supone que debe.

 1
Author: Roy Dictus,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-05-05 12:33:44

Cree un método, por ejemplo, llamado IsUserNameValid() y haga que sea accesible desde cualquier lugar. Yo mismo lo pondría en el servicio al usuario. Hacer esto no te limitará cuando surjan cambios futuros. Mantiene el código de validación en un solo lugar (implementación), y otro código que depende de él no tendrá que cambiar si cambia la validación Puede encontrar que necesita llamar a esto desde varios lugares más adelante, como la interfaz de usuario para la indicación visual sin tener que recurrir a la excepción manejo. La capa de servicio para las operaciones correctas, y el repositorio (cache, db, etc.) capa para asegurar que los elementos almacenados son válidos.

 0
Author: Charles Lambert,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-05-07 02:07:51

Me gusta la opción 3. La implementación más simple podría verse así:

public interface IUser
{
    string Name { get; }
    bool IsNew { get; }
}

public class User : IUser
{
    public string Name { get; private set; }
    public bool IsNew { get; private set; }
}

public class UserService : IUserService
{
    public void ValidateUser(IUser user)
    {
        var repository = RepositoryFactory.GetUserRepository(); // use IoC if needed

        if (user.IsNew && repository.UserExists(user.Name))
            throw new ValidationException("Username already exists");
    }
}
 0
Author: SlavaGu,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-05-08 11:23:19

Crear servicio de dominio

O puedo crear un servicio de dominio para creación y edición de usuarios. Pero alguien puede editar directamente el nombre del usuario sin llamar a ese servicio...

Si diseñaste correctamente tus entidades, esto no debería ser un problema.

 -1
Author: epitka,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2011-04-28 14:50:59