Programação Funcional em C# — Parte 03

Alex Alves
6 min readMar 15, 2021

Ir para Parte 02

Vamos então falar de um “side effect” específico?

Exceções são um tipo de “side effects”, onde, muitas vezes, não conseguimos controlar o que será disparado como exceção em uma aplicação.

Diante disso, vamos falar sobre as técnicas/preferências com relação a utilização do bloco try…catch:

  • Comumente, podemos notar a utilização do bloco try…catch em camadas de níveis mais alto, como em controllers. Isso se faz na intenção de garantir que nenhuma exceção será estourada, em um determinado fluxo, onde poderá fazer com que sua aplicação “morra”.
  • E, algumas vezes, podemos perceber a utilização do bloco try…catch em níveis mais baixos, como em conexões externas, na intenção de já prevenir algum possível erro já conhecido.

Em ambas as situações você consegue resolver o problema de não matar sua aplicação devido a uma exceção inesperada. Mas vamos analisá-los melhor:

  • O primeiro caso, você corre o risco de perder a rastreabilidade da exceção e não retornar uma mensagem mais plausível/factível para o seu usuário.
  • No segundo caso, você consegue ir um pouco mais além. Além de capturar a exceção em um nível mais baixo e conhecido, você consegue tratá-la (como a implementação de algum retry) e disparar algum retorno compatível com a exceção.

Tendo esse cenário em vista, ainda assim não conseguimos ter uma programação funcional neste caso. Vamos ver um exemplo mais claro:

Podemos notar que exceções é algo que viola tal paradigma, tornando o método não transparente. Além disso, você não consegue controlar o ponto de parada das exceções, pois ela é disparada e percorre a pilha de execução (recursivamente) até encontrar um bloco try…catch e isso, podemos assimilar ao velho comando “goto”, o qual aprendemos na faculdade que não é uma boa prática e as chances de você perder o fluxo do processo é grande.

Bom, sabemos do problema, agora como podemos resolvê-lo? Como falado desde o início, precisamos deixar o mais explícito possível todos os resultados de retorno, deixando a assinatura do método mais coesa. Isso indica que, ao invés de dispararmos alguma exceção, retornamos algo que substitua esta ação:

Sendo assim, conseguimos deixar claro quais são as entradas e saídas do método, tendo total controle sobre ele e como utilizar tal retorno.

Visto sobre com as exceções funcionam e como podemos contornar seu uso, vamos refletir alguns casos sobre elas:

  • Exceções devem ser algo excepcional no seu projeto
  • Exceções devem ser algo sinalizador de bugs
  • Exceções nunca devem ser utilizadas em algo já previamente conhecido (como é o caso do exemplo de validação mostrado acima)

Tendo essa reflexão em vista e o exemplo mostrado acima, vamos analisar o seguinte “qual o melhor local para colocar tais validações”. Esta provocação é muito plausível pois, não faz sentido ter algo do tipo em uma camada de negócio, por exemplo. Pois caso o nome ou a idade for inválido e chegar na camada de negócio (a qual irá utilizar os dados, de fato), significa que irá gerar um bug para o seu negócio e um exceção deve ser sim lançada. Contudo, podemos prevenir isso utilizando o Fail Fast Principle, o qual prevê:

  • Falhar o mais breve possível
  • Interromper imediatamente do fluxo
  • Deixar explícito a falha

E isto, podemos aplicar nas camadas externas, antes de chegar na camada de domínio. Ou seja, ao receber algum input do usuário ou realizar algum tipo de comunicação externa, por exemplo, podemos realizar alguma tratativa para retornar algo mais amigável, como no exemplo acima, onde retornamos uma string. Sendo assim, caso tenha algo inválido, explicitamos isso para quem chamou sua aplicação o mais rápido possível.

Bom, mas ainda assim as exceções irão existir 🤔. Não há como fugir delas… Como falado anteriormente, muitas vezes podem ocorrer de maneiras inesperadas. Utilizar exceções em camadas de níveis mais altos é algo que não pode ser descartado, pois pode ter casos que você não consiga tratar/controlar um erro no sistema. Contudo, podemos melhorar as tratativas de exceções em níveis mais baixos, quando se faz o uso de alguma biblioteca terceira, como a utilização de um framework para se conectar em um banco de dados ou para realizar a comunicação em uma API.

Vejamos um exemplo:

No método acima, não há nenhuma tratativa de exceção ao tentar incluir algum registro em um banco de dados. Caso ocorra algum erro, como perda de conexão com o banco de dados, uma exceção será lançada e a mesma será capturada, provavelmente, na camada de mais alto nível, tendo o efeito “goto”.

Uma alternativa seria o código abaixo:

Existe um bloco try…catch genérico, porém englobando a conexão/operação com o banco de dados. Mas ainda assim, tal método não transmite total confiança, pois o fluxo não será interrompido e o erro só será percebido caso alguém analise os logs.

Visando ainda melhorar este cenário, podemos ter a seguinte opção:

Onde deixamos claro se a operação de incluir um registro no banco foi ou não efetivada, com um retorno explícito. Com isso, deixamos a assinatura do método mais coesa e conseguimos ler/entender o que ele fará e os possíveis retornos.

Com isso, conseguimos interromper alguma propagação de exceção específica e ainda realizar alguma tratativa para explicitar um erro e deixar claro para o usuário o que ocorreu.

Ainda assim, conseguimos melhorar nossa tratativa, pois, pode ser que um simples booleano ou string como retorno não seja suficiente. Com isso, podemos então ter uma classe Result que conterá o status da operação e, caso haja falha, uma mensagem de erro e/ou mais algum dado que possa ser relevante para o contexto. Podemos comparar isso como um retorno “envelopado”.

Bom, mas tudo o que vimos agora, sobre como realizar tratativas de exceções, não viola o CQS que vimos anteriormente? Como que um método que salvará algo no banco de dados terá um retorno? Há uma contradição nisto, porém tendo um retorno explícito e padronizado, aumenta a legibilidade de nosso código.

Sendo assim, além de podermos saber se um método é um comando ou uma consulta, você consegue saber se tal método pode ou não falhar, amplificando a assinatura do método com informações relevantes:

Imagem retirada do curso Applying Functional Principles in C#

Antes de finalizarmos este capítulo, gostaria de ressaltar um ponto importante:

  • Para realizar tratativas de bibliotecas terceiras, sempre verifique quais tipos possíveis de exceções elas podem lançar (verificando na assinatura do método da biblioteca), para realizar ações específicas.

Com isso terminamos nossa série sobre Programação Funcional! Caso tenha alguma dúvida ou feedback, deixe nos comentários! 😁

--

--

Alex Alves

Bachelor in Computer Science, MBA in Software Architecture and .NET Developer.