26 dezembro, 2016 11 Comentários AUTOR: elemarjr CATEGORIAS: CSharp, Programação Tags:

Melhorando validações no domínio com melhores assinaturas de método e lógica funcional (Parte 2)

Tempo de leitura: 4 minutos

O post anterior dividiu opiniões. Houve quem gostasse da abordagem que eu indiquei. Também houve quem achasse complexa demais.

Embora eu tenha deixado claro, logo no início, que havia selecionado um exemplo trivial apenas para poder apresentar conceitos, um bocado de gente ignorou tal aviso.

Pois bem, o post de hoje trata da aplicação dos conceitos apresentados no post anterior em cenários "do mundo real".

Cenário

Imaginemos um exemplo simples, porém, relacionado com o código que escrevemos no dia-a-dia. Pensemos em 1) recuperar uma entidade do banco de dados, 2) aplicar uma modificação e 3) persistir a entidade modificada.

Como você implementaria isso?

Uma abordagem frequente

Um caminho muito comum para resolver esse cenário seria algo semelhante ao indicado nesse esboço:

public class EmployeeRepository
{
    /* .. */
    public Employee GetById(string id) {  /* .. */ }
    public void Save(Employee employee) {  /* .. */ }
    /* .. */
}

public class Employee
{
    /* .. */
    public void RaiseSalary(decimal amount) {  /* .. */ }
}

Há aqui um repositório (que é um serviço do domínio) que consegue recuperar e persistir a entidade que está sendo manipulada (no exemplo Employee). Também há uma entidade com um método que revela a motivação da mudança.

Parece estar tudo certo, não? Um exemplo de consumo poderia ser:

class DummySalaryService
{
    private readonly EmployeeRepository _repository;

    public DummySalaryService(EmployeeRepository repository)
    {
        _repository = repository;
    }

    public void RaiseSalaryOfEmployee(string employeeId, decimal amount)
    {
        var employee = _repository.GetById(employeeId);

        if (employee == null)
        {
            // deveríamos lançar uma Exception?
        }

        employee.RaiseSalary(amount);
        _repository.Save(employee);
    }
}

Você percebe as fragilidades desse código?

Crítica a forma como a entidade é recuperada

Vamos nos concentrar, por um breve instante, na forma como a entidade é recuperada.


public Employee GetById(string id) {  /* .. */ }

O que essa assinatura nos diz?

Basicamente, sabemos que devemos passar o Id (que é um string) da entidade que desejamos recuperar e receberemos uma instância da entidade materializada.

Mas, por essa assinatura, me responda:

  1. O que ocorre caso seja fornecido um Id inválido (null, por exemplo)?
  2. O que ocorre caso não exista uma entidade correspondente ao Id fornecido? Será lançada uma exception? Será retornado null?
  3. O que ocorre se houver um problema de conexão com o banco de dados?

Por convenção, você poderia afirmar que a passagem de um parâmetro inválido deveria disparar um ArgumentException, certo? Mas, se o cliente dessa API passar null, será disparada uma ArgumentNullException?

Caso o parâmetro fornecido seja válido, mas sem correspondência, o serviço retornará um null? Lançará uma Exception?

Muitas perguntas em aberto. Muitas respostas que serão conferidas apenas em tempo de execução ou inspecionando o código e facilmente ignoradas por programadores descuidados. Para mim, isso explica o porquê de tantos erros ridículos em tempo de execução.

Tornando a assinatura mais clara

Pois bem, utilizando os tipos elevados que apresentei no post anterior (com implementação no GitHub, caso tenha interesse), compartilho uma implementação mais coerente:

public Try<Exception, Employee> GetById(Untrusted<string> id) { /* .. */ }

O que essa assinatura está nos dizendo:

  1. Haverá uma tentativa de recuperar a entidade. Essa tentativa irá retornar a entidade, caso ela exista e tudo ocorrer bem, ou uma exception caso, por algum motivo, não seja possível recuperar a entidade.
  2. O parâmetro que será recebido será tratado como pouco confiável. Afinal, não há garantias de que o Id esteja formatado adequadamente.

Ainda melhor seria enriquecer o modelo de domínio com uma nova primitiva representando um Id natural.

public Try<Exception, Employee> GetById(Cpf cpf) { /* .. */ }

Estou afastando a possibilidade de retornar null para um id sem entidade correspondente.

Crítica e proposta para a modificação do estado da entidade

A implementação da entidade proposta originalmente está, honestamente, melhor do que aquelas que tenho encontrado no "mundo real". Há um método que revela claramente a "motivação para a mudança" no lugar de uma propriedade estúpida com um setter anêmico. Entretanto, nem por isso, está livre de críticas.

Implementações mutáveis conduzem ao desenvolvimento de código difícil de manter, principalmente em cenários onde seja necessário suportar concorrência. Por isso, há tempos, venho usando entidades imutáveis.

Além disso, a assinatura original oculta o fato de que, caso uma regra de negócio seja violada, uma exception poderia ser disparada.

Minha proposta aqui seria:

public class Employee
{
    /* .. */
    public Try<Exception, Employee> RaiseSalary(decimal amount) {  /* .. */ }
}

A modificação que estou propondo indica claramente que o método poderia resultar em uma exception ou em uma nova instância de Employee com o valor atualizado.

Crítica e proposta para a persistência da entidade

Por fim, tempos a persistência da entidade.

Bertrand Meyer propôs que métodos deveriam operar como comandos, alterando o estado da entidade, ou como consultas, retornando alguma coisa. Logo, a proposta original para persistência da entidade parece correta.

public void Save(Employee employee) {  /* .. */ }

A verdade, porém, não é tão simples. Esse método pode (e deve) disparar uma exception caso algo inesperado ocorra. Mas, ele não deixa isso claro.

Minha proposta seria:

public Try<Exception, Unit> Save(Employee employee) {  /* .. */ }

O que estou propondo aqui é deixar claro que esse método pode falhar e forçar quem está implementando a oferecer tratamento adequado.

O tipo Unit, usado aqui, é uma implementação vazia, de marcação, usada em lugar de void.

A versão revisada

A versão que eu recomendo para resolver o cenário proposto seria assim:

public class EmployeeRepository
{
    /* .. */
    public Try<Exception, Employee> GetById(Untrusted<string> id) {  /* .. */ }
    public Try<Exception, Unit> Save(Employee employee) {  /* .. */ }
    /* .. */
}

public class Employee
{
    /* .. */
    public Try<Exception, Employee> RaiseSalary(decimal amount) {  /* .. */ }
}

Aqui, o código todo indica claramante o que esperar de cada implementação.

Mas, o melhor está na implementação do código cliente.

class DummySalaryService
{
    private readonly EmployeeRepository _repository;

    public DummySalaryService(EmployeeRepository repository)
    {
        _repository = repository;
    }

    public Try<Exception, Unit> RaiseSalaryOfEmployee(string employeeId, decimal amount)
    {
        return _repository.GetById(employeeId)
            .Bind(employee => employee.RaiseSalary(amount))
            .Bind(updatedEmployee => _repository.Save(updatedEmployee));
    }
}

Dessa vez, o método está indicando claramente que pode falhar. Entretanto, o código segue um fluxo lógico de sucesso (Não há contaminações com tratamento de falhas).

Se o nome Bind (que é um operador funcional) te incomoda, pode recorrer a um simplificador

public static class TryExtensions
{
    public static Try<TFailure, TSuccessResult> Then<TFailure, TSuccess, TSuccessResult>(
        this Try<TFailure, TSuccess> that,
        Func<TSuccess, Try<TFailure, TSuccessResult>> func
    ) => that.Bind(func);
}

Quem sabe, pode até escrever algo assim:

class DummySalaryService
{
    private readonly EmployeeRepository _repository;

    public DummySalaryService(EmployeeRepository repository)
    {
        _repository = repository;
    }

    public Try<Exception, Unit> RaiseSalaryOfEmployee(string employeeId, decimal amount)
        => _repository.GetById(employeeId)
            .Then(employee => employee.RaiseSalary(amount))
            .Then(updatedEmployee => _repository.Save(updatedEmployee));
}

Masturbação funcional? Complexidade desnecessária? Sério? Eu vejo código mais expressivo e com muito menos chances de falha em tempo de execução.

Era isso.

11 Comentários

  1. José Roberto Araújo 2 meses ago says:

    Elemar, como havia mencionado no seu primeiro Post, sobre implementação de conceitos funcionais em código C#, como forma de dar mais expressividade, garantia da qualidade e minimizar os cenários de erro. Reafirmo minha posição e opnião sobre esses conceitos dentro de linguagens que são essencialmente orientadas a objetos: Entendo que são muito úteis, necessárias e fazem o código falar muito mais sobre o que cada ponto do sistema pretende resolver, além de deixar os métodos mais honestos sobre o que pretendem receber de parâmetros e fornecer como retorno.
    Parabéns pelo seu trabalho, compartilhando sempre conosco seus: Conhecimentos e Implementações interessantes.

    Responder
    • elemarjr 2 meses ago says:

      Muito obrigado pelo feedback.

      Responder
  2. Emerson Soares 2 meses ago says:

    Elemar, além de toda a abordagem utilizada para melhorar a expressividade da implementação gosto muito quando você nos lembra que repositórios são serviços de domínio. Já tive discussões sobre repositórios serem ou não serviços de domínio... Existe uma confusão em torno desta definição, talvez pelo fato de os repositórios serem serviços com um propósito muito bem definido. Partindo daí, eu acredito que o repositório como um serviço que é deve abstrair todo conhecimento que diz respeito a manter o estado do agregado. Existem situações em que, por definição o processo exige a garantia de que o agregado exista, como é o caso de `RaiseSallary` no seu exemplo, e não faz sentido que o serviço lide com o fato de o funcionário não existir. Explico: em uma aplicação que lida com meios de pagamento tenho o evento `UserProfileCreated`. Através da API de pagamentos os clientes podem criar `PaymentRequests` que por sua vez são associadas a um email de usuário. O usuário pode ou não existir na aplicação. Sempre que o evento `UserProfileCreated` ocorre eu tenho que associar aquele `UserProfile` a todos os seus pagamentos já criados. No momento em que este evento ocorre já sabemos que o `UserProfile` existe e por isto não faz sentido que o repositório retorne `null`, neste caso implemento o método `UserProfile Load(Email userEmail);` que retorna um `MissingUserProfileException` caso ele não exista. Entendo este metódo apenas como uma forma de carregar o estado do agregado para que eu possa executar operações nele. Porém, existem situações em que faz parte do processo a existencia ou não do agregado. Como por exemplo, sempre que o evento `PaymentRequestOpened` ocorre, o sistema deve associar o `UserProfile` ao `PaymentRequest` caso ele exista. Neste caso, por natureza o domínio assume o fato de que o `UserProfile` pode não existir. Aqui implemento o método: `bool Find(Email userEmail, out UserProfile);` que deixa claro que o `UserProfile` pode não existir. Com esta abordagem meu repositório tem dois metódos que podem retornar um `UserProfile`: Load e Find. Acredito que sua abordagem se encaixa como uma luva para a melhoria de expressividade nesta implementação também. Parabéns e obrigado por compartilhar.

    Responder
    • elemarjr 2 meses ago says:

      Obrigado pelo feedback.
      Considere dar uma olhada no tipo `Option` para o método `Find`

      Responder
  3. Osmar Brito 2 meses ago says:

    Elemar, esse exemplo está melhor já que trouxe um cenário de um mundo real, mas me diga a exception seria evitada ou ela seria lançada e automaticamente tratada? Por que eu acho feio programar orientado a tratamento de exceções, se não existe forma de evitar e sabemos que uma hora ou outra teremos alguma não é melhor deixar a aplicação tratar ela de forma elegante ao invés de expressar essa preocupação o tempo todo via código?

    Responder
  4. Guilherme Matheus Costa 2 meses ago says:

    Elemar, não conhecia o Bertrand Meyer, você poderia me indicar a literatura em que você encontrou essa proposta? Ademais, desde o primeiro artigo notei riqueza neste design, parabéns pelos artigos! 🙂

    Responder
  5. Henrique Costa 2 meses ago says:

    Olá Elemar, tudo bem ? Perdoe minha ignorância, sou um estudante ainda (acho que serei eternamente, rs) Estou acompanhando está série e pelo que entendi, o objetivo primario desta abordagem, além de deixar as assinaturas mais claras do que pode vir a ocorrer no método , é eliminar ou pelo menos diminuir side-effects ? Como se faz em programação funcional ? Outro ponto , pelo que você esta nos mostrando, o Try/Catch fica meio "inutil" haja vista que podemos simplesmente mostrar na função a lista de exceptions que pode ocorrer , essa era uma das intençoes ? E o "throw" nunca seria disparado, em nenhum momento, certo ?

    Responder
    • elemarjr 2 meses ago says:

      Exato!

      Responder
  6. Roger Lozada 1 mês ago says:

    Boa tarde Elemar,
    Eu gostei dessa forma de fazer a montagem, realmente o resultado final ficou com uma ótima leitura.
    O que me incomoda nesse exemplo é parar de retornar os metódos somente com o tipo deles e passar a retornar Try para tudo.
    Talvez para quem já esteja acostumado com a programação funcional fique bem tranquilo de assimilar, porém, para quem não conhece a programação funcional não me parece uma boa idéia.
    Antes de mais nada, eu testei bem o exemplo para ver como ficava a codificação e a partir daí que tirei minha conclusão.
    Em resumo, excelente artigo, é sempre bom aprender mais de uma forma de se fazer a mesma coisa, parabéns.

    Responder