"Escreva o código de uma nova forma (tm)"





Não gosto de C #, mas gosto de coletar todos os padrões e todo o açúcar que eles oferecem de versão para versão.



No terceiro dia, assisti ao discurso de Bill Wagner nas conferências da NDC , onde ele mostrou que você precisa escrever código de uma nova maneira (TM).



Ele mostra muitos exemplos de boa refatoração, o código se torna mais legível, mas a partir daquele momento percebi que a linguagem precisa de um arquiteto são.



Açúcar não vai ajudar em nada



Pegue um pedaço de código mal escrito escrito por um amador em seus joelhos. Este método verifica o estado da instância da classe e retorna verdadeiro se estiver ok e falso se não estiver.



internal bool GetAvailability()
{
    if (_runspace.RunspacePoolAvailability == RunspacePoolAvailability.Available) { return true;}
    if (_runspace.RunspacePoolAvailability == RunspacePoolAvailability.Busy) { return true;}
    return false;
}
      
      





O programador tentou, nem mesmo uma única coisa no método. Mas somos experientes, vamos refatorá-lo, remover os ifs e transformá-lo em um ternark:



internal bool GetAvailability()
{
    return _runspace.RunspacePoolAvailability == RunspacePoolAvailability.Available ||
           _runspace.RunspacePoolAvailability == RunspacePoolAvailability.Busy;
}
      
      





Ficou muito melhor, 2 linhas de código em vez de 5, mas o ternark pode ser transformado em um padrão:



internal bool GetAvailability()
{
    return _runspace.RunspacePoolAvailability is RunspacePoolAvailability.Available or RunspacePoolAvailability.Busy;
}
      
      





No total, deixamos uma bela linha de código. Tudo! A refatoração está completa! (não)



internal void Invoke()
{
	if (!GetAvailability()) return;
        PowerShell _powershell = PowerShell.Create();
        _powershell.RunspacePool = _runspace;
        _powershell.Invoke()
    
}
      
      





Chamar _powershell'a em um _runspace'e inacessível lançará uma exceção.



Todas as implementações são igualmente ruins porque ela resolve um problema - protege o código por trás de si mesmo para que não lance uma exceção, e sua aparência já é a décima coisa.



O código não estava atualizado, mas seu significado não mudou.



Mais exceções!



Quando um programa encontra a realidade, é claro, surgem situações excepcionais. Arquivo não encontrado, arquivo com formato errado, conteúdo errado, sem conteúdo, Streamreader leu nulo ou passou para uma string vazia. Duas linhas de código foram escritas e ambas quebradas, mas assisti à palestra e consegui enxergar.



“Mas se preocupe, agora, ao fazer sua própria classe ou biblioteca, você não precisa pensar no código defensivo, o compilador faz a verificação de tipo para nós e verificar se há null nunca foi tão fácil!



Basta jogar tudo para o usuário da biblioteca e escrever o código. Lançar exceções e colocar um programa agora se tornou prestigioso! Eu jogo e você pega! "


Que, como entendi a palestra de Bill Wagner - NDC Conferences 2020,



fiquei tão inspirado por este conceito, e pelo trabalho de .net em geral, então vou contar a vocês a verdadeira história do desenvolvimento das classes RunspacePool e Powershell do System .Management.Automation, que encontrei recentemente:



Smoker # 1 faz Powershell



Em primeiro lugar, é claro, para manter o controle do estado, criamos um campo booleano que muda para true quando o método Dispose é chamado.



Geralmente não é seguro mostrar o campo IsDisposed, porque se o CLR coletar lixo, você poderá capturar uma referência nula.



class PowershellInNutshell() : IDisposable
{
    private static bool IsDisposed = false;
    private static RunspacePoolInTheNuttshell;

    public static void Invoke()
    {
        if (IsDisposed) throw new ObjectDisposedException();
        Console.WriteLine("I was invoked");
    }

    public void Dispose()
    {
        if (IsDisposed) throw new ObjectDisposedException("Invoke","   ,      ,  ");
        IsDisposed = true;
        Console.WriteLine("I was invoked");
        GC.SuppressFinalize(this);
    }
}
      
      





Quando chamamos Dispose ou outro método novamente, lançamos uma exceção e deixamos o outro programador também monitorar o estado da instância ou capturar exceções com seu código, mas esses não são meus problemas.



Fumante nº 2 ganha RunspacePooll



Aqui também criamos o campo IsDisposed, mas desta vez o fazemos com um getter público, para que a pessoa que usa a biblioteca não precise escrever mais código de segurança.



class RunspacePoolInTheNuttshell() : IDisposable
{
    public static bool IsDisposed = false;
    
    public void Dispose()
    {
        if (IsDisposed) return;
        IsDisposed = true;
        GC.SuppressFinalize(this);
        Console.WriteLine("I was invoked");
    }
}
      
      





Se Dispose foi chamado, volte e pronto. Claro que ao acessar novamente o campo ele receberá nullref, pois o objeto já estará retirado da memória, mas isso é problema meu.



Uma pessoa saudável usa a biblioteca:



Aqui está uma exceção, aqui não há exceção, aqui embrulhamos o peixe. Ambas as classes vêm no mesmo pacote e têm comportamentos diferentes. As classes lançam o mesmo tipo de exceção por motivos diferentes.



  • Senha incorreta? InvalidRunspacePoolStateException!
  • Sem conexão? InvalidRunspacePoolStateException!


Acontece que em um lugar você precisa lidar com ObjectDisposedException, em outra NullReferenceException na terceira InvalidRunspacePoolStateException, e tudo está cheio de surpresas.



A exceção não é uma solução





Antes da comunhão das sagradas ordenanças, eu li o arquivo da maneira antiga:



public static void Main()
{
    string txt = @"c:\temp\test.txt";
    
    if (File.Exists(txt)) return;
    string readText = File.ReadAllText(txt);
    Console.WriteLine(readText);
}
      
      





Mas depois de assistir ao vídeo, comecei a fazer de uma nova maneira:



public static void Main()
{
    string txt = @"c:\temp\test.txt";
    try
    {
        string readText = File.ReadAllText(txt);
        Console.WriteLine(readText);
    }
    catch (System.IO.FileNotFoundException)
    {
        Console.WriteLine("File was not found");
    }
}
      
      





Ou é uma nova maneira?



public static void Main()
{
    string txt = @"c:\temp\test.txt";

    if (!File.Exists(txt))
    {
        throw new NullReferenceException();
    }
    string readText = File.ReadAllText(txt);
    Console.WriteLine(readText);
}
      
      





Como exatamente é de uma nova maneira? Onde termina a responsabilidade do desenvolvedor e começa a responsabilidade do usuário?





Em geral, a ideia é clara, se você é o autor da biblioteca, pode imediatamente chamar o método e enviar dados incorretos para ele, você conhece a área de assunto perfeitamente e descreveu todos os casos de comportamento incorreto, descartou exceções que sentir e lidar com eles você mesmo.



internal class NewWay
{
    public static string _a;
    public static string _b;
    public static string _c;

    public static void NewWay(string a, string b, string c)
    {
        string _a = a ?? throw new NullReferenceException("a is null");
        string _b = b ?? throw new NullReferenceException("b is null");
        string _c = c ?? throw new NullReferenceException("c is null");
    }
    public void Print()
    {
        if (String.Compare(_a, _b) != 0)
        {
            throw new DataException("Some Other Ex");
        }

        Console.WriteLine($"{_a + _b + _c}");// 
    }
}
      
      





try
{
    NewWay newway = new(stringThatCanBeNull, stringThatCanBeNull, stringThatCanBeNull);
    newway.Print();
}
catch (NullReferenceException ex)
{
    Console.WriteLine(" ");
}
catch (DataException ex)
{
    Console.WriteLine(" ");
}

      
      





Os mais engenhosos já entenderam para onde estou levando. A organização da correção de erros com base em blocos try catch levará apenas a um aninhamento de código mais profundo.



Usando esse padrão, em qualquer caso, nos recusamos a executar o código, mas de forma mais educada.



Em geral, nada de novo, há 10 anos as pessoas começaram a suspeitar que o C # estava sobrecarregado de padrões e de ano para ano eles não diminuíam. Conheça outro, lançar exceções ficou mais fácil.



E finalmente - os operadores





Os operadores não devem lançar nada em qualquer lugar.



Um exemplo de JS que você provavelmente conhece:



console.log('2'+'2'-'2');
// 20
      
      



 

Os designers de JS consideraram que um operador de adição separado e um operador de concatenação separado não são necessários, portanto, fazer matemática em JS não é seguro.



A fonte desse bug em JS é a conversão implícita do tipo de string para o tipo int usando o operador. É assim que o excesso de açúcar se torna um bug.



C # também sofre de conversão implícita de tipo, embora com muito menos frequência. Tomemos, por exemplo, a entrada do usuário, que, após a atualização da biblioteca, passou a ser mapeada para string em vez de int, como antes, e o operador (+) e o operador matemático e o operador de concatenação. 



Alterar o tipo de int para string não quebrou o código, mas quebrou a lógica de negócios.



Portanto, vou deixar aqui, e você tenta adivinhar o resultado da execução sem executar. 



class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine($"{'2' + '2' - '2' }");
    }
}
      
      








All Articles