"Escribe el código de una manera nueva (tm)"





No me gusta C #, pero me gusta recopilar todos los patrones y todo el azúcar que ofrecen de una versión a otra.



El tercer día vi el discurso de Bill Wagner en NDC Conferences , donde mostró que es necesario escribir código de una manera nueva (TM).



Muestra muchos ejemplos de buena refactorización, el código se vuelve más legible, pero desde ese momento me di cuenta de que el lenguaje necesita un arquitecto cuerdo.



El azúcar no ayudará en las cosas



Tome un fragmento de código mal escrito escrito por un aficionado en su rodilla. Este método verifica el estado de la instancia de la clase y devuelve verdadero si está bien y falso si no.



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





El programador lo intentó, ni siquiera uno más en el método. Pero tenemos experiencia, refactorémoslo, eliminemos los if y conviértalo en un ternark:



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





Se ha vuelto mucho mejor, 2 líneas de código en lugar de 5, pero el ternark se puede convertir en un patrón:



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





En total, hemos dejado una hermosa línea de código. ¡Todo! ¡La refactorización está completa! (no)



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





Llamar a _powershell'a en un _runspace'e inaccesible generará una excepción.



Todas las implementaciones son igualmente malas porque resuelve un problema: protege el código detrás de sí mismo para que no arroje una excepción, y la forma en que se ve ya es la décima cosa.



El código no estaba actualizado, pero su significado no ha cambiado.



¡Más excepciones!



Cuando un programa se encuentra con la realidad, por supuesto, surgen situaciones excepcionales. Archivo no encontrado, archivo de formato incorrecto, contenido incorrecto, sin contenido, Streamreader leyó nulo o lo pasó a una cadena vacía. Se escriben dos líneas de código y ambas están rotas, pero miré la charla y pude ver.



“Pero preocúpese, ahora, al crear su propia clase o biblioteca, no tiene que pensar en el código defensivo, el compilador hace la verificación de tipos por nosotros, ¡y verificar nulos nunca ha sido tan fácil!



Simplemente arroje todo al usuario de la biblioteca y escriba el código. ¡Lanzar excepciones y poner un programa ahora se ha vuelto prestigioso! ¡Yo tiro y tú atrapas! "


Que, según entendí la charla de Bill Wagner - NDC Conferences 2020,



me inspiró tanto este concepto, y el trabajo de .net en general, así que les contaré la verdadera historia del desarrollo de las clases RunspacePool y Powershell de System. .Management.Automation, que encontré recientemente:



Smoker # 1 hace Powershell



En primer lugar, por supuesto, para realizar un seguimiento del estado, creamos un campo booleano que cambia a verdadero cuando se llama al método Dispose.



Por lo general, no es seguro mostrar el campo IsDisposed, porque si CLR recolecta basura, puede capturar una referencia 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);
    }
}
      
      





Cuando volvemos a llamar a Dispose u otro método, lanzamos una excepción y dejamos que el otro programador también controle el estado de la instancia o detecte excepciones con su código, pero estos no son mis problemas.



Smoker # 2 hace RunspacePooll



Aquí también creamos el campo IsDisposed, pero esta vez lo hacemos con un getter público, para que la persona que usa la biblioteca no tenga que escribir más código de seguridad.



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





Si se ha llamado a Dispose, regrese y listo. Por supuesto, al volver a acceder al campo, recibirá nullref, porque el objeto ya se eliminará de la memoria, pero este es mi problema.



Una persona sana usa la biblioteca:



Aquí hay una excepción, aquí no hay excepción, aquí envolvemos el pescado. Ambas clases vienen en el mismo paquete y tienen un comportamiento diferente. Las clases lanzan el mismo tipo de excepción por diferentes razones.



  • ¿Contraseña incorrecta? InvalidRunspacePoolStateException!
  • ¿Sin conexión? InvalidRunspacePoolStateException!


Resulta que en un lugar necesitas manejar ObjectDisposedException, en otro NullReferenceException en la tercera InvalidRunspacePoolStateException, y todo está lleno de sorpresas.



La excepción no es una solución





Antes de la comunión de las santas ordenanzas, leí el archivo a la antigua:



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





Pero después de ver el video, comencé a hacer de una manera nueva:



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");
    }
}
      
      





¿O es una nueva forma?



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);
}
      
      





¿Cómo es exactamente de una manera nueva? ¿Dónde termina la responsabilidad del desarrollador y comienza la responsabilidad del usuario?





En general, la idea es clara, si usted es el autor de la biblioteca, entonces puede llamar inmediatamente al método y enviarle datos incorrectos, conoce perfectamente el área temática y describió todos los casos de comportamiento incorrecto, descartó excepciones que hacen Sentir y manejarlos usted mismo.



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(" ");
}

      
      





Los más ingeniosos ya han entendido a dónde me dirijo. La organización de la corrección de errores basada en bloques try catch solo conducirá a un anidamiento de código más profundo.



Usando este patrón, en cualquier caso, nos negamos a ejecutar el código, pero de manera más cortés.



En general, nada nuevo, hace 10 años la gente empezó a sospechar que C # estaba sobrecargado de patrones y de año en año no bajaban. Conoce a otro, lanzar excepciones ahora es más fácil.



Y finalmente - los operadores





Los operadores no deberían lanzar nada en ningún lado.



Un ejemplo de JS que probablemente conozcas:



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



 

Los diseñadores de JS consideraron que no se necesitan un operador de adición y un operador de concatenación separados, por lo que hacer cálculos matemáticos en JS no es seguro.



La fuente de este error en JS es la conversión implícita del tipo de cadena al tipo int usando el operador. Así es como el exceso de azúcar se convierte en un error.



C # también sufre de conversión de tipos implícita, aunque con mucha menos frecuencia. Tomemos, por ejemplo, la entrada del usuario, que, después de actualizar la biblioteca, comenzó a mapearse en cadena en lugar de int, como antes, y el operador (+) y el operador matemático y el operador de concatenación. 



Cambiar el tipo de int a string no rompió el código, pero rompió la lógica empresarial.



Así que lo dejo aquí, e intenta adivinar el resultado de la ejecución sin ejecutar. 



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








All Articles