C# - Fail Fast (Falhando rápido)


  Hoje veremos o princípio 'Fail Fast' ou 'Falhe Rápido' e como usá-lo no C#.

O termo 'Fail Fast'  refere-se a  um princípio de design de software que se aplica a muitas linguagens de programação, incluindo C#. O conceito do "Fail Fast" enfatiza a detecção e o tratamento precoce de erros e problemas no código, a fim de melhorar a confiabilidade e a capacidade de diagnóstico do software

Aqui estão algumas das ideias-chave associadas ao "Fail Fast" em C# e na plataforma .NET:

  1. Detecção Precoce de Erros: O "Fail Fast" envolve a ideia de que, quando um erro é detectado, o sistema deve falhar imediatamente, em vez de continuar a execução com um estado de erro desconhecido. Isso ajuda a identificar problemas o mais cedo possível no ciclo de vida do software.
  2. Exceções: Em C# e na plataforma .NET, as exceções são frequentemente usadas para implementar o "Fail Fast". Quando ocorre um erro, uma exceção é lançada, interrompendo a execução normal do programa e permitindo que os desenvolvedores capturem e lidem com o erro de maneira apropriada.
  3. Logs Detalhados: Para facilitar a depuração e a resolução de problemas, é comum registrar informações detalhadas sobre as exceções e os erros que ocorrem no sistema. Isso ajuda a identificar a causa raiz do problema.
  4. Validação de Entrada: Uma prática comum no "Fail Fast" é validar e verificar as entradas do usuário ou de outras partes do sistema o mais cedo possível. Se os dados de entrada não atenderem aos critérios de validação, um erro é gerado imediatamente.
  5. Testes Unitários: O "Fail Fast" é muitas vezes complementado por testes unitários rigorosos. Os testes unitários são projetados para identificar erros rapidamente, garantindo que os componentes individuais do software funcionem corretamente.

A seguir temos exemplos de aplicação deste princípio:

1- Validação de Parâmetros em Funções:

Uma maneira comum de aplicar o "Fail Fast" é validar os parâmetros de uma função. Se um parâmetro não atender aos critérios de validação, uma exceção é lançada imediatamente.

public class Calculadora
{
 
public int Dividir(int dividendo, int divisor)
  {
   
if (divisor == 0)
    {
       
throw new ArgumentException("Divisor não pode ser zero.");
    }
   
return dividendo / divisor;
  }
}

Neste exemplo, se alguém tentar chamar o método Dividir com um divisor igual a zero, uma exceção será lançada imediatamente.

2- Validação de Entrada de Usuário:

Em um aplicativo da web, você pode aplicar o "Fail Fast" para validar a entrada do usuário antes de processá-la.

public IActionResult UpdateProfile(string username, string email)
{
 
if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(email))
  {
   
// Falha imediatamente se os campos estiverem vazios.
   
return BadRequest("Nome de usuário e email são obrigatórios.");
  }
  
// Continue com a atualização do perfil.
}

Aqui, o aplicativo verifica se os campos de nome de usuário e email estão vazios e, se estiverem, retorna uma resposta de erro imediatamente.

3- Manipulação de Arquivos:

Quando você trabalha com arquivos, o "Fail Fast" pode ser aplicado para garantir que os arquivos necessários estejam presentes antes de prosseguir.

public void ProcessaArquivo(string caminhoArquivo)
{
  
if (!File.Exists(caminhoArquivo))
   {
    
throw new FileNotFoundException("O arquivo não existe.", caminhoArquivo);
   }
  
// Continue com o processamento do arquivo.
}

Neste caso, se o arquivo especificado não existir, uma exceção FileNotFoundException será lançada imediatamente.

4- Validação de Configurações:

No início da aplicação, é importante validar as configurações do aplicativo para garantir que tudo esteja configurado corretamente.

public void InitializeApp()
{
  
string apiKey = ConfigurationManager.AppSettings["ApiKey"];
  
if (string.IsNullOrWhiteSpace(apiKey))
   {
      
throw new ConfigurationException("A chave de API não foi configurada.");
   }
   
// Continue com a inicialização da aplicação.
}

Se a chave de API não estiver configurada, a aplicação falhará na inicialização.

Agora vou mostrar um exemplo onde não escrevemos nenhuma validação e onde estamos obtendo uma string de conexão de nossa configuração e usando-a para adicionar um banco de dados Postgres à coleção de serviços de nosso aplicativo ASP.NET Core.

public void ConfigureServices(IServiceCollection services)
{
  
var connStr = Configuration.GetConnectionString("DefaultConnection");
   services.AddDbContext<AppDbContext>(options => options.UseNpgsql(connStr));
}

Usando este código em algum momento de nossa inicialização, garantimos que o banco de dados seja migrado usando:

 context.Database.Migrate();

Agora, se você executar seu código sem configurar a string de conexão corretamente, receberá o seguinte erro:

System.ArgumentException: Host não pode ser nulo

Você pode se perguntar: “O que diabos isso significa? Qual argumento ? Qual Host ?”

A razão pela qual você está ficando confuso e a mensagem é tão enigmática é que não fizemos a devida diligência e validamos a entrada de uma fonte externa assim que a recebemos.

Neste caso, não validamos se a string de conexão retirada da Configuração realmente existe. Ao fazer isso, garantimos que o aplicativo falhará o mais tarde possível, em um momento imprevisível e em condições imprevisíveis.

Você vai perder um bom tempo para detectar este problema.

A solução

A solução é simples. NÃO codifique defensivamente. Falhe RÁPIDO. Falhe informativamente. Lançe exceções.

Ao lidar com qualquer entrada, pergunte-se:

'Essa entrada é crítica para o escopo com o qual você está trabalhando (aplicativo, solicitação HTTP, manipulador de barramento de eventos...)?'

Se sim, valide e, se não for o esperado, lance uma exceção imediatamente

public void ConfigureServices(IServiceCollection services)
{
 
var connStr = Configuration.GetConnectionString("DefaultConnection");
 
if (string.IsNullOrEmpty(connStr))
  {
    
throw new ConfigurationMissingException("DefaultConnection");
  }
  services.AddDbContext<AppDbContext>(options => options.UseNpgsql(connStr));
}

Ou :

...
builder.Services.AddDbContext<ProductServiceContext>(options =>
          options.UseSqlServer(builder.Configuration.GetConnectionString(
"ProductServiceContext")
          ??
throw new InvalidOperationException("Connection string 'ProductServiceContext' not found.")));
...

Dificultando a depuração

Uma maneira ainda melhor de garantir que nosso aplicativo falhe de maneira imprevisível em momentos imprevisíveis é programar de forma excessivamente defensiva e introduzir valores padrão e alternativos para entradas críticas.

Programar de forma excessivamente defensiva e introduzir valores padrão e alternativos pode levar a problemas como:

  1. Complexidade desnecessária: Introduzir muita lógica defensiva e valores padrão pode tornar o código difícil de entender e manter.
  2. Má utilização de recursos: Introduzir valores padrão pode levar a alocação desnecessária de recursos, como memória e processamento, quando esses valores não são necessários.
  3. Comportamento imprevisível: Introduzir valores padrão pode levar a um comportamento imprevisível quando os valores reais são ausentes ou inválidos.
  4. Dificuldade em identificar erros reais: A sobrecarga de tratamento defensivo pode tornar difícil distinguir erros reais de problemas de validação.

Considere clássico que ilustra isso em ação pode ser visto no código a seguir:

public class UserAuthentication
{
 
public bool AuthenticateUser(string username, string password)
  {
    
// Verificação defensiva
    
if (username == null)
     {
          username =
"defaultUser";
     }
    
if (password == null)
     {
          password =
"defaultPassword";
     }
    
// Simulação de autenticação
    
if (username == "admin" && password == "admin123")
     {
      
return true;
     }
    
return false;
  }
}

class Program
{
 
static void Main()
  {
    UserAuthentication auth =
new UserAuthentication();
   
// Tenta autenticar um usuário
   
bool isAuthenticated = auth.AuthenticateUser(null, null);
   
if (isAuthenticated)
    {
       Console.WriteLine(
"Usuário autenticado com sucesso.");
    }
   
else
    {
       Console.WriteLine(
"Falha na autenticação.");
    }
  }
}

Neste exemplo, a classe UserAuthentication introduz verificações defensivas para verificar se os valores de entrada username e password são nulos. Se forem nulos, valores padrão são atribuídos. No entanto, isso introduz complexidade desnecessária e obscurece o motivo real da falha na autenticação.

Quando executamos o programa com auth.AuthenticateUser(null, null), o resultado é que o usuário é autenticado com sucesso, embora não tenhamos fornecido credenciais válidas. Isso é devido à lógica defensiva e à atribuição de valores padrão. Como resultado, a depuração se torna complicada, pois é difícil determinar por que o usuário foi autenticado quando as credenciais estavam ausentes.

Em vez de programar de forma excessivamente defensiva com valores padrão, é preferível falhar imediatamente quando as entradas críticas são nulas ou inválidas, seguindo o princípio "Fail Fast". Isso torna a depuração mais simples e ajuda a identificar e resolver problemas com mais eficácia.

E estamos conversados...

E Jesus, respondendo, disse-lhes: Não necessitam de médico os que estão sãos, mas, sim, os que estão enfermos;
Eu não vim chamar os justos, mas, sim, os pecadores, ao arrependimento.
Lucas 5:31,32

Referências:


José Carlos Macoratti