Onde devemos colocar a validação para o modelo de domínio

38

Eu ainda estou procurando as melhores práticas para validação de modelo de domínio. É bom colocar a validação no construtor do modelo de domínio? meu exemplo de validação de modelo de domínio da seguinte maneira:

public class Order
 {
    private readonly List<OrderLine> _lineItems;

    public virtual Customer Customer { get; private set; }
    public virtual DateTime OrderDate { get; private set; }
    public virtual decimal OrderTotal { get; private set; }

    public Order (Customer customer)
    {
        if (customer == null)
            throw new  ArgumentException("Customer name must be defined");

        Customer = customer;
        OrderDate = DateTime.Now;
        _lineItems = new List<LineItem>();
    }

    public void AddOderLine //....
    public IEnumerable<OrderLine> AddOderLine { get {return _lineItems;} }
}


public class OrderLine
{
    public virtual Order Order { get; set; }
    public virtual Product Product { get; set; }
    public virtual int Quantity { get; set; }
    public virtual decimal UnitPrice { get; set; }

    public OrderLine(Order order, int quantity, Product product)
    {
        if (order == null)
            throw new  ArgumentException("Order name must be defined");
        if (quantity <= 0)
            throw new  ArgumentException("Quantity must be greater than zero");
        if (product == null)
            throw new  ArgumentException("Product name must be defined");

        Order = order;
        Quantity = quantity;
        Product = product;
    }
}

Obrigado por todas as suas sugestões.

adisembiring
fonte

Respostas:

47

Há um artigo interessante de Martin Fowler sobre esse assunto que destaca um aspecto que a maioria das pessoas (inclusive eu) tende a ignorar:

Mas uma coisa que eu acho que constantemente engana as pessoas é quando elas pensam na validade do objeto de uma maneira independente do contexto, como implica um método isValid.

Eu acho que é muito mais útil pensar em validação como algo vinculado a um contexto - normalmente uma ação que você deseja executar. Este pedido é válido para ser preenchido, este cliente é válido para fazer check-in no hotel. Portanto, em vez de ter métodos como isValid, temos métodos como isValidForCheckIn.

A partir disso, o construtor não deve fazer a validação, exceto talvez algumas verificações muito básicas de sanidade compartilhadas por todos os contextos.

Novamente do artigo:

Em About Face, Alan Cooper defendeu que não devemos deixar que nossas idéias de estados válidos impeçam um usuário de inserir (e salvar) informações incompletas. Lembrei-me disso há alguns dias atrás ao ler um rascunho de um livro em que Jimmy Nilsson está trabalhando. Ele afirmou um princípio de que você sempre deve salvar um objeto, mesmo que ele tenha erros. Embora eu não esteja convencido de que essa deva ser uma regra absoluta, acho que as pessoas tendem a evitar economizar mais do que deveriam. Pensar no contexto da validação pode ajudar a impedir isso.

Michael Borgwardt
fonte
Graças a Deus alguém disse isso. Formulários com 90% dos dados, mas que não salvam nada, são injustos para os usuários, que geralmente compõem os outros 10% apenas para não perder dados; portanto, toda a validação feita é forçar o sistema a perder o controle dos quais 10% foi inventado. Problemas semelhantes podem ocorrer no back-end - digamos, uma importação de dados. Descobri que geralmente é melhor tentar trabalhar corretamente com dados inválidos do que impedir que isso aconteça.
Psd
2
@psr Você precisa de lógica de back-end se seus dados não persistirem? Você pode deixar toda a manipulação no lado do cliente se seus dados não tiverem significado no seu modelo de negócios. Também seria desperdício de recursos para enviar e receber mensagens (cliente - servidor) se os dados não tiverem sentido. Então, voltamos à idéia de "nunca permitir que objetos de domínio entrem em estado inválido!" .
Geo C.
2
Eu me pergunto por que tantos votos para uma resposta tão ambígua. Ao usar o DDD, às vezes existem algumas regras que simplesmente verificam se alguns dados são INT ou estão dentro de um intervalo. Por exemplo, quando você permite que o usuário do aplicativo escolha algumas restrições em seus produtos (quantas vezes alguém pode visualizar meu produto e em que dias o intervalo de um mês). Aqui, ambas as restrições devem ser int e uma delas deve estar no intervalo de 0 a 31. Parece validação de formato de dados que, em um ambiente não DDD, caberia em um serviço ou controlador. Mas, no DDD, estou do lado de manter a validade no domínio (90%).
Geo C.
2
Forçar as camadas superiores a saberem muito sobre o domínio para mantê-lo em um estado válido cheira a mau design. O domínio deve ser aquele que garante que seu estado seja válido. Mover muito sobre os ombros das camadas superiores pode tornar seu domínio anêmico e você pode eliminar algumas restrições importantes que podem prejudicar seus negócios. O que eu percebo agora, uma generalização adequada seria manter sua validação o mais próximo possível da sua persistência ou o mais próximo do seu código de manipulação de dados (quando é manipulado para atingir um estado final).
Geo C.
PS: Não misturo autorização (é permitido fazer alguma coisa), autenticação (a mensagem veio do local certo ou foi enviada pelo cliente certo, ambos sendo identificados por chave api / token / nome de usuário ou qualquer outra coisa) com validação de formato ou regras de negócios. Quando digo 90%, refiro-me às regras de negócios que a maioria delas também inclui validação de formato. A validação do formato ofcourse pode estar nas camadas superiores, mas a maioria estará no domínio (mesmo formato de endereço de email que será validado no objeto de valor EmailAddress).
Geo C.
6

Apesar de esta questão ser um pouco obsoleta, gostaria de acrescentar algo que vale a pena:

Gostaria de concordar com @MichaelBorgwardt e estender trazendo à tona a testabilidade. Em "Trabalhando efetivamente com o código herdado", Michael Feathers fala muito sobre obstáculos aos testes e um desses obstáculos é "difícil de construir" objetos. A construção de um objeto inválido deve ser possível e, como sugere Fowler, as verificações de validade dependentes do contexto devem ser capazes de identificar essas condições. Se você não conseguir descobrir como construir um objeto em um equipamento de teste, terá problemas ao testar sua classe.

Quanto à validade, gosto de pensar em sistemas de controle. Os sistemas de controle funcionam analisando constantemente o estado de uma saída e aplicando ações corretivas à medida que a saída se desvia do ponto de ajuste, isso é chamado de controle de malha fechada. O controle de loop fechado intrinsecamente espera desvios e age para corrigi-los e é assim que o mundo real funciona, e é por isso que todos os sistemas de controle reais geralmente usam controladores de loop fechado.

Eu acho que o uso de validação dependente de contexto e objetos fáceis de construir facilitará o trabalho do sistema no futuro.

Paulo
fonte
1
Muitas vezes, os objetos parecem difíceis de construir. Por exemplo, neste caso, você pode ignorar o construtor público criando uma classe Wrapper que herda da classe que está sendo testada e permite criar uma instância do objeto base em um estado inválido. É aqui que o uso dos modificadores de acesso corretos em classes e construtores entra em jogo e pode ser realmente prejudicial ao teste, se usado incorretamente. Além disso, evitar classes e métodos "selados", exceto onde apropriado, ajudará muito a tornar o código mais fácil de testar.
P. Roe
4

Como eu tenho certeza que você já sabe ...

Na programação orientada a objetos, um construtor (às vezes reduzido para ctor) em uma classe é um tipo especial de sub-rotina chamada na criação de um objeto. Ele prepara o novo objeto para uso, geralmente aceitando parâmetros que o construtor usa para definir quaisquer variáveis ​​de membro necessárias quando o objeto é criado pela primeira vez. É chamado de construtor porque constrói os valores dos membros de dados da classe.

A verificação da validade dos dados passados ​​como parâmetros c'tor é definitivamente válida no construtor - caso contrário, você está possivelmente permitindo a construção de um objeto inválido.

No entanto (e esta é apenas a minha opinião, não é possível encontrar bons documentos neste momento) - se a validação de dados exigir operações complexas (como operações assíncronas - talvez validação com base no servidor se estiver desenvolvendo um aplicativo de desktop), é melhor coloque uma função de inicialização ou validação explícita de algum tipo e os membros configurem os valores padrão (como null) no c'tor.


Além disso, apenas como uma nota lateral, como você a incluiu no seu exemplo de código ...

A menos que você esteja realizando uma validação adicional (ou outra funcionalidade) AddOrderLine, provavelmente exporia a List<LineItem>propriedade como propriedade, em vez de Orderagir como fachada .

Demian Brecht
fonte
Por que expor o contêiner? O que importa para as camadas superiores o que é o contêiner? É perfeitamente razoável ter um AddLineItemmétodo. De fato, para DDD, isso é preferido. Se List<LineItem>for alterado para um objeto de coleção personalizado, a propriedade exposta e tudo o que dependia de uma List<LineItem>propriedade estão sujeitos a alterações, erros e exceções.
precisa saber é o seguinte
4

A validação deve ser realizada o mais rápido possível.

A validação em qualquer contexto, seja no modelo de domínio ou em qualquer outra forma de escrever software, deve servir ao propósito do QUE você deseja validar e em que nível você está no momento.

Com base na sua pergunta, acho que a resposta seria dividir a validação.

  1. A validação da propriedade verifica se o valor dessa propriedade está correto, por exemplo, quando um intervalo entre 1 e 10 é excedido.

  2. A validação de objeto garante que todas as propriedades no objeto sejam válidas em conjunto. por exemplo, BeginDate é anterior a EndDate. Suponha que você leia um valor do armazenamento de dados e BeginDate e EndDate sejam inicializados em DateTime.Min por padrão. Ao definir o BeginDate, não há motivo para aplicar a regra "deve ser anterior ao fim do dia", pois isso não se aplica a YET. Esta regra deve ser verificada APÓS todas as propriedades terem sido definidas. Isso pode ser chamado no nível raiz agregado

  3. A validação também deve ser realizada na entidade agregada (ou raiz agregada). Um objeto Order pode conter dados válidos, assim como OrderLines. Porém, uma regra comercial afirma que nenhum pedido pode exceder US $ 1.000. Como você aplicaria essa regra em alguns casos, isso é permitido. você não pode simplesmente adicionar uma propriedade "não validar quantia", pois isso levaria a abusos (mais cedo ou mais tarde, talvez até você, apenas para tirar esse "pedido desagradável" do caminho).

  4. Em seguida, há validação na camada de apresentação. Você realmente enviará o objeto pela rede, sabendo que ele falhará? Ou você poupará o usuário desse ônus e o informará assim que ele inserir um valor inválido. por exemplo, na maioria das vezes o seu ambiente DEV será mais lento que a produção. Gostaria de esperar 30 segundos antes de ser informado de "você esqueceu esse campo novamente durante mais uma execução de teste", especialmente quando há um bug de produção a ser corrigido com seu chefe respirando pelo pescoço?

  5. A validação no nível de persistência deve estar o mais próximo possível da validação do valor da propriedade. Isso ajudará a evitar exceções com a leitura de erros "nulo" ou "valor inválido" ao usar mapeadores de qualquer tipo ou simples leitores de dados antigos. O uso de procedimentos armazenados resolve esse problema, mas requer escrever novamente a mesma lógica de valiação e executá-la novamente. E os procedimentos armazenados são o domínio do administrador do banco de dados, portanto, não tente fazer o trabalho do HIS também (ou pior, incomode-o com essa "escolha minuciosa pela qual ele não está sendo pago").

então, com algumas palavras famosas "depende", mas pelo menos agora você sabe POR QUE depende.

Eu gostaria de poder colocar tudo isso em um único lugar, mas, infelizmente, isso não pode ser feito. Fazer isso colocaria uma dependência em um "objeto Deus" contendo TODA a validação para TODAS as camadas. Você não quer seguir esse caminho sombrio.

Por esse motivo, apenas lancei exceções de validação em um nível de propriedade. Todos os outros níveis que utilizo ValidationResult com um método IsValid para reunir todas as "regras quebradas" e passá-las ao usuário em uma única AggregateException.

Ao propagar a pilha de chamadas, reuno-as novamente em AggregateExceptions até atingir a camada de apresentação. A camada de serviço pode lançar essa exceção diretamente para o cliente no caso do WCF como uma FaultException.

Isso permite que eu pegue a exceção e divida-a para mostrar erros individuais em cada controle de entrada ou achatá-la e mostrá-la em uma única lista. A escolha é sua.

é por isso que também mencionei a validação da apresentação, para curto-circuitos, tanto quanto possível.

Caso você esteja se perguntando por que também tenho a validação no nível de agregação (ou no nível de serviço, se quiser), é porque não tenho uma bola de cristal me dizendo quem usará meus serviços no futuro. Você terá problemas suficientes para encontrar seus próprios erros para impedir que outros cometam seus erros :) digitando dados inválidos. Por exemplo, você administra o aplicativo A, mas o aplicativo B alimenta alguns dados usando seu serviço. Adivinha quem eles perguntam primeiro quando há um bug? O administrador do aplicativo B terá prazer em informar o usuário "não há erro no meu final, apenas insiro os dados".

Wesley Kenis
fonte