Validação e autorização em arquitetura em camadas

13

Eu sei que você está pensando (ou talvez gritando) ", nenhuma outra pergunta perguntando onde a validação pertence a uma arquitetura em camadas?!?" Bem, sim, mas espero que isso seja um pouco diferente do assunto.

Acredito firmemente que a validação assume muitas formas, é baseada em contexto e varia em cada nível da arquitetura. Essa é a base para o pós - ajudar a identificar que tipo de validação deve ser realizada em cada camada. Além disso, uma pergunta que costuma surgir é onde pertencem as verificações de autorização.

O cenário de exemplo vem de um aplicativo para uma empresa de catering. Periodicamente, durante o dia, o motorista pode entregar ao escritório qualquer excesso de dinheiro acumulado ao levar o caminhão de um local para outro. O aplicativo permite que o usuário registre o 'dinheiro' coletando a identificação do motorista e o valor. Aqui está um código de esqueleto para ilustrar as camadas envolvidas:

public class CashDropApi  // This is in the Service Facade Layer
{
    [WebInvoke(Method = "POST")]
    public void AddCashDrop(NewCashDropContract contract)
    {
        // 1
        Service.AddCashDrop(contract.Amount, contract.DriverId);
    }
}

public class CashDropService  // This is the Application Service in the Domain Layer
{
    public void AddCashDrop(Decimal amount, Int32 driverId)
    {
        // 2
        CommandBus.Send(new AddCashDropCommand(amount, driverId));
    }
}

internal class AddCashDropCommand  // This is a command object in Domain Layer
{
    public AddCashDropCommand(Decimal amount, Int32 driverId)
    {
        // 3
        Amount = amount;
        DriverId = driverId;
    }

    public Decimal Amount { get; private set; }
    public Int32 DriverId { get; private set; }
}

internal class AddCashDropCommandHandler : IHandle<AddCashDropCommand>
{
    internal ICashDropFactory Factory { get; set; }       // Set by IoC container
    internal ICashDropRepository CashDrops { get; set; }  // Set by IoC container
    internal IEmployeeRepository Employees { get; set; }  // Set by IoC container

    public void Handle(AddCashDropCommand command)
    {
        // 4
        var driver = Employees.GetById(command.DriverId);
        // 5
        var authorizedBy = CurrentUser as Employee;
        // 6
        var cashDrop = Factory.CreateCashDrop(command.Amount, driver, authorizedBy);
        // 7
        CashDrops.Add(cashDrop);
    }
}

public class CashDropFactory
{
    public CashDrop CreateCashDrop(Decimal amount, Employee driver, Employee authorizedBy)
    {
        // 8
        return new CashDrop(amount, driver, authorizedBy, DateTime.Now);
    }
}

public class CashDrop  // The domain object (entity)
{
    public CashDrop(Decimal amount, Employee driver, Employee authorizedBy, DateTime at)
    {
        // 9
        ...
    }
}

public class CashDropRepository // The implementation is in the Data Access Layer
{
    public void Add(CashDrop item)
    {
        // 10
        ...
    }
}

Eu indiquei 10 locais onde vi verificações de validação inseridas no código. Minha pergunta é: quais verificações você executaria, se houver alguma, em cada uma das seguintes regras de negócios (juntamente com verificações padrão de comprimento, intervalo, formato, tipo etc.):

  1. O valor da queda de caixa deve ser maior que zero.
  2. O saque em dinheiro deve ter um driver válido.
  3. O usuário atual deve estar autorizado a adicionar retiradas de caixa (o usuário atual não é o motorista).

Compartilhe seus pensamentos, como você tem ou abordaria esse cenário e os motivos de suas escolhas.

SonOfPirate
fonte
SE não é exatamente a plataforma certa para "promover uma discussão teórica e subjetiva". Votação para fechar.
tdammers
Declaração mal formulada. Estou realmente procurando as melhores práticas.
SonOfPirate
2
@ Tdammers - Sim, é o lugar certo. Pelo menos ele quer ser. No FAQ: 'Perguntas subjetivas são permitidas'. É por isso que eles criaram este site em vez do Stack Overflow. Não seja um nazista próximo. Se a pergunta for péssima, ela desaparecerá na obscuridade.
usar o seguinte código
@ Fastast: Não é tanto a parte 'subjetiva', mas a 'discussão' que me incomoda.
tdammers
Eu acho que você poderia aproveitar objetos de valor aqui por ter um CashDropAmountobjeto de valor em vez de usar a Decimal. Verificar se o driver existe ou não seria feito no manipulador de comandos e o mesmo vale para as regras de autorização. Você pode obter autorização de graça, fazendo algo como Approver approver = approverService.findById(employeeId)onde ele joga, se o funcionário não estiver na função de aprovador. Approverseria apenas um objeto de valor, não uma entidade. Você também pode se livrar de sua fábrica ou uso método de fábrica em uma AR em vez disso: cashDrop = driver.dropCash(...).
Plalx

Respostas:

2

Concordo que o que você está validando será diferente em cada camada do aplicativo. Normalmente, valido apenas o necessário para executar o código no método atual. Tento tratar os componentes subjacentes como caixas pretas e não valido com base em como esses componentes são implementados.

Então, como exemplo, na sua classe CashDropApi, eu apenas verificaria que 'contrato' não é nulo. Isso evita NullReferenceExceptions e é tudo o que é necessário para garantir que esse método seja executado corretamente.

Não sei se validaria algo nas classes de serviço ou comando e o manipulador verificaria apenas que 'comando' não é nulo pelos mesmos motivos que na classe CashDropApi. Eu vi (e fiz) a validação nos dois sentidos, para as classes factory e entidade. Um ou outro é o local em que você deseja validar o valor de 'amount' e que os outros parâmetros não são nulos (suas regras de negócios).

O repositório deve apenas validar se os dados contidos no objeto são consistentes com o esquema definido no seu banco de dados e a operação daa será bem-sucedida. Por exemplo, se você tem uma coluna que não pode ser nula ou tem um comprimento máximo, etc.

Quanto à verificação de segurança, acho que é realmente uma questão de intenção. Como a regra se destina a impedir o acesso não autorizado, eu gostaria de fazer essa verificação o mais cedo possível para reduzir o número de etapas desnecessárias que eu tomei se o usuário não estiver autorizado. Eu provavelmente colocaria no CashDropApi.

jpm70
fonte
1

Sua primeira regra de negócios

O valor da queda de caixa deve ser maior que zero.

parece um invariante da sua CashDropentidade e da sua AddCashDropCommandclasse. Existem algumas maneiras de impor um invariante como este:

  1. Siga a rota Design por contrato e use os contratos de código com uma combinação de pré-condições, pós-condições e um [ContractInvariantMethod], dependendo do seu caso.
  2. Escreva um código explícito no construtor / setters que lança uma ArgumentException se você passar uma quantidade menor que 0.

Sua segunda regra é de natureza mais ampla (à luz dos detalhes da pergunta): válido significa que a entidade Condutor possui uma bandeira indicando que pode dirigir (ou seja, não teve sua carteira de motorista suspensa), significa que o motorista estava realmente trabalhando naquele dia ou significa simplesmente que o driverId, passado para o CashDropApi, é válido no armazenamento de persistência.

Em qualquer um desses casos, você precisará navegar no seu modelo de domínio e obter a Driverinstância do seu IEmployeeRepository, como faz emlocation 4 no exemplo de código. Portanto, aqui você precisa garantir que a chamada para o repositório não retorne nulo; nesse caso, seu driverId não era válido e você não poderá prosseguir com o processamento.

Para as outras 2 verificações (minhas hipotéticas) (o motorista possui uma carteira de motorista válida, o motorista estava trabalhando hoje), você está executando as regras de negócios.

O que costumo fazer aqui é usar uma coleção de classes de validadores que operam em entidades (assim como o padrão de especificação do livro de Eric Evans - Domain Driven Design). Eu usei o FluentValidation para criar essas regras e validadores. Posso então compor (e, portanto, reutilizar) regras mais complexas / mais completas a partir de regras mais simples. E posso decidir quais camadas da minha arquitetura devem ser executadas. Mas eu tenho todos eles codificados em um só lugar, não espalhados pelo sistema.

Sua terceira regra está relacionada a uma preocupação transversal: autorização. Como você já está usando um contêiner de IoC (assumindo que seu contêiner de IoC ofereça suporte à interceptação de método), você pode fazer alguma AOP . Escreva um apsect que faça a autorização e você pode usar seu contêiner de IoC para injetar esse comportamento de autorização onde ele precisa estar. A grande vitória aqui é que você escreveu a lógica uma vez, mas pode reutilizá-la em todo o sistema.

Para usar a interceptação por meio de um proxy dinâmico (Castle Windsor, Spring.NET, Ninject 3.0, etc), sua classe de destino precisa implementar uma interface ou herdar de uma classe base. Você interceptaria antes da chamada para o método de destino, verificaria a autorização do usuário e impediria que a chamada prosseguisse para o método real (lance uma exceção, log, retorne um valor indicando falha ou algo mais) se o usuário não tiver as funções certas para executar a operação.

No seu caso, você pode interceptar a chamada para

CashDropService.AddCashDrop(...) 

AddCashDropCommandHandler.Handle(...)

Os problemas aqui talvez CashDropServicenão possam ser interceptados porque não há interface / classe base. Ou AddCashDropCommandHandlernão está sendo criado pelo seu IoC, portanto, seu IoC não pode criar um proxy dinâmico para interceptar a chamada. O Spring.NET possui um recurso útil onde você pode direcionar um método para uma classe em uma montagem por meio de uma expressão regular, portanto, isso pode funcionar.

Espero que isso te dê algumas ideias.

RobertMS
fonte
Você pode explicar como eu "usaria seu contêiner de IoC para injetar esse comportamento de autorização onde ele precisa estar"? Isso parece atraente, mas fazer com que AOP e IoC trabalhem juntos me escapa até agora.
SonOfPirate
Quanto ao resto, concordo em colocar a validação no construtor e / ou nos levantadores para impedir que o objeto entre em um estado inválido (manipulando invariantes). Mas, além disso, e uma referência à verificação nula depois de ir ao IEmployeeRepository para localizar o driver, você não fornece nenhum detalhe onde você executaria o restante da validação. Dado o uso de FluentValidation e a reutilização, etc, ele fornece, onde você aplicaria as regras no modelo fornecido?
21712 SonOfPirate
Eu editei minha resposta - veja se isso ajuda. Quanto a "onde você aplicaria as regras no modelo fornecido?"; provavelmente em torno de 4, 5, 6, 7 no seu manipulador de comandos. Você tem acesso aos repositórios que podem produzir as informações necessárias para executar a validação no nível de negócios. Mas acho que há outros que discordam de mim aqui.
precisa saber é o seguinte
Para esclarecer, todas as dependências estão sendo injetadas. Deixei isso para manter o código de referência breve. Minha pergunta tem mais a ver com ter uma dependência dentro do aspecto, pois os aspectos não são injetados pelo contêiner. Então, como o AuthorizationAspect obtém uma referência ao AuthorizationService, por exemplo?
SonOfPirate
1

Para as regras:

1- O valor da queda de caixa deve ser maior que zero.

2- O saque em dinheiro deve ter um motorista válido.

3- O usuário atual deve estar autorizado a adicionar saques em dinheiro (o usuário atual não é o motorista).

Eu faria a validação no local (1) da regra de negócios (1) e me certificaria de que o ID não fosse nulo ou negativo (supondo que zero seja válido) como verificação prévia da regra (2). O motivo é minha regra de "Não ultrapasse o limite de uma camada com dados errados que você pode verificar com as informações disponíveis". Uma exceção a isso seria se o serviço fizer a validação como parte de seu dever para com outros chamadores. Nesse caso, será suficiente ter a validação apenas lá.

Para as regras (2) e (3), isso deve ser feito na camada de acesso ao banco de dados (ou na própria camada db) apenas uma vez que envolve o acesso db. Não é necessário viajar intencionalmente entre as camadas.

Em particular, a regra (3) pode ser evitada se permitirmos que a GUI impeça que usuários não autorizados apertem o botão que habilita esse cenário. Embora isso seja mais difícil de codificar, é melhor.

Boa pergunta!

NoChance
fonte
+1 para autorização - colocá-lo na interface do usuário é uma alternativa que não mencionei na minha resposta.
precisa saber é o seguinte
Embora as verificações de autorização na interface do usuário forneçam uma experiência mais interativa para o usuário, estou desenvolvendo uma API baseada em serviço e não posso fazer suposições sobre quais regras o chamador implementou ou não. É porque muitas dessas verificações podem ser facilmente delegadas à interface do usuário que eu escolhi usar o projeto da API como base para a postagem. Estou procurando práticas recomendadas em vez de livros didáticos de maneira fácil e rápida.
SonOfPirate
@SonOfPirate, INMO, a interface do usuário precisa fazer validações porque é mais rápida e possui mais dados que o serviço (em alguns casos). Agora, o serviço não deve enviar dados fora de seus limites sem fazer suas próprias validações, pois isso faz parte de suas responsabilidades desde que você queira que o serviço não confie no cliente. Portanto, sugiro que sejam feitas verificações não-db no serviço (novamente) antes de enviar dados ao banco de dados para processamento adicional.
NoChance