"Write the code in a new way (tm)"





I don't like C #, but I like to collect all the patterns and all the sugar they offer from version to version.



The third day I watched Bill Wagner's speech at NDC Conferences , where he showed that you need to write code in a new way (TM).



He shows many examples of good refactoring, the code becomes more readable, but from that moment I realized that the language needs a sane architect.



Sugar won't help matters



Take a poorly written piece of code written by an amateur on his knee. This method checks the state of the class instance and returns true if ok and false if not.



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





The programmer tried, not even a single else in the method. But we are experienced, let's refactor it, remove the ifs and turn it into a ternark:



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





It has become much better, 2 lines of code instead of 5, but the ternark can be turned into a pattern:



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





In total, we have left one beautiful line of code. Everything! Refactoring is complete! (not)



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





Calling _powershell'a in an inaccessible _runspace'e will throw an exception.



All implementations are equally bad because it solves one problem - it protects the code behind itself so that it does not throw an exception, and the way it looks is already the tenth thing.



The code was not up to date, but its meaning has not changed.



More exceptions!



When a program encounters reality, of course, exceptional situations arise. File not found, file of wrong format, wrong content, no content, Streamreader read null or passed it on to an empty string. Two lines of code are written and both are broken, but I watched the talk and got my sight.



“But worry, now, when creating your own class or library, you don't have to think about the defensive code, the compiler does the typecheck for us, and checking for null has never been easier!



Just throw everything onto the user of the library and write the code. Throwing exceptions and putting a program has now become prestigious! I throw, and you catch! "


That, as I understood the talk by Bill Wagner - NDC Conferences 2020



I was so inspired by this concept, and the work of .net in general, so I will tell you the true story of the development of the RunspacePool and Powershell classes from System.Management.Automation, which I recently encountered:



Smoker # 1 makes Powershell



First of all, of course, to keep track of the state, we make a boolean field that changes to true when the Dispose method is called.



It is generally unsafe to show the IsDisposed field, because if the CLR collects garbage, you can catch a Null reference.



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





When we call Dispose or another method again, we throw an exception and let another programmer also track the state of the instance or catch exceptions with his code, but these are not my problems.



Smoker # 2 makes RunspacePooll



Here we also make the IsDisposed field, but this time we make it with a public getter, so that the person using the library does not have to write more security code.



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





If Dispose has been called, return and be done. Of course, when re-accessing the field, it will receive nullref, because the object will already be removed from memory, but is this my problem.



A healthy person uses the library:



Here is an exception, here is no exception, here we wrap the fish. Both classes come in the same package and have different behavior. Classes throw the same type of exception for different reasons.



  • Wrong password? InvalidRunspacePoolStateException!
  • No connection? InvalidRunspacePoolStateException!


It turns out that in one place you need to handle ObjectDisposedException, in another NullReferenceException in the third InvalidRunspacePoolStateException, and everything is full of surprises.



Exception is not a solution





Before the communion of the holy ordinances, I read the file in the old way:



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





But after watching the video, I started doing in a new way:



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





Or is it a new way?



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





How exactly is it in a new way? Where does developer responsibility end and user responsibility begin?





In general, the idea is clear, if you are the author of the library, then you can immediately call the method and submit incorrect data to it, you know the subject area perfectly and described all cases of incorrect behavior, threw out exceptions that make sense and handled them yourself.



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

      
      





The most ingenious have already understood where I am leading. Organization of error correction based on try catch blocks will only lead to deeper code nesting.



Using this pattern, in any case, we refuse to execute the code, but more politely.



In general, nothing new, 10 years ago people began to suspect that C # was overloaded with patterns and from year to year they did not decrease. Meet another one, throwing exceptions just got easier.



And finally - the operators





Operators shouldn't cast anything anywhere.



An example from JS you probably know:



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



 

The JS designers considered that a separate addition operator and a separate concatenation operator are not needed, so doing math in JS is unsafe.



The source of this bug in JS is the implicit conversion of the string type to the int type using the operator. This is how excess sugar becomes a bug.



C # also suffers from implicit type casting, albeit much less often. Take, for example, the user input, which, after updating the library, began to be mapped to string instead of int, as before, and the operator (+) and the mathematical operator and the concatenation operator. 



Changing the type from int to string did not break the code, but broke the business logic.



So I'll leave it here, and you try to guess the result of the execution without running. 



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








All Articles