«Пишите код по-новому (тм)»

dgoteuauz-prkvy1q1wigufxhc0.png

C# я не люблю, но люблю собирать все паттерны и весь сахар, который они предлагают от версии к версии.

Третьего дня посмотрел выступление Билла Вагнера на NDC Conferences, где он показывал, что нужно писать код по-новому .

Он показывает много примеров хорошего рефакторинга, код становится более читаемым, но именно с этого момента я понял, что языку нужен вменяемый архитектор.

Сахаром делу не поможешь


Возьмем плохо написанный фрагмент кода, который написал любитель на коленке. Этот метод проверяет состояние экземпляра класса и возвращает true, если все хорошо и false, если не хорошо.
internal bool GetAvailability()
{
    if (_runspace.RunspacePoolAvailability == RunspacePoolAvailability.Available) { return true;}
    if (_runspace.RunspacePoolAvailability == RunspacePoolAvailability.Busy) { return true;}
    return false;
}

Программист старался, даже ни одного else в методе. Но мы то опытные, давайте зарефакторим его, уберем if«ы и превратим это в тернарку:
internal bool GetAvailability()
{
    return _runspace.RunspacePoolAvailability == RunspacePoolAvailability.Available ||
           _runspace.RunspacePoolAvailability == RunspacePoolAvailability.Busy;
}

Стало гораздо лучше, 2 строки кода вместо 5, но тернарку можно превратить в паттерн:
internal bool GetAvailability()
{
    return _runspace.RunspacePoolAvailability is RunspacePoolAvailability.Available or RunspacePoolAvailability.Busy;
}

Итого мы оставили одну красивую строчку кода. Все! Рефакторинг завершен! (нет)
internal void Invoke()
{
	if (!GetAvailability()) return;
        PowerShell _powershell = PowerShell.Create();
        _powershell.RunspacePool = _runspace;
        _powershell.Invoke()
    
}

Вызов _powershell«а в недоступном _runspace«e вызовет исключение.

Все реализации одинаково плохи потому, что он решают одну задачу — защищает код за собой, чтобы тот не выдал исключение, а то, как он выглядит это уже десятое дело.

Код был не современен, но его смысл не поменялся.

Больше исключений!


Когда программа сталкивается с реальностью, конечно, возникают исключительные ситуации. Файл не найден, файл не того формата, не того содержимого, содержимого нет, Streamreader прочитал null или пустую строку передал это дальше. Написаны две строки кода и обе сломаны, но я посмотрел выступление и прозрел.
«Но беспокойтесь, теперь, делая свой собственный класс или библиотеку вы можете не думать о защитном коде, тайпчек за нас делает компилятор, а проверка на null никогда не была проще!

Просто скиньте все на пользователя библиотеки и пишите код. Выкидывать исключения и класть программу теперь стало престижно! Я кидаю, а ты лови!»


Тот, как я понял доклад Билла Вагнера — NDC Conferences 2020

Я был настолько вдохновлен этой концепцией, да и работой .net в целом, поэтому расскажу вам правдивую историю разработки классов RunspacePool и Powershell из System.Management.Automation, с которыми я недавно столкнулся:

Курильщик №1 делает Powershell


Первым делом, конечно, чтобы отслеживать, состояние мы делаем булёвое поле, которое изменяется на true при вызове метода Dispose.

Показывать поле IsDisposed в принципе небезопасно, потому что, если CLR соберет мусор, можно словить 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);
    }
}

При повторном вызове Dispose или другого метода мы выкидываем исключение и пусть другой программист еще и своим кодом отслеживает состояние экземпляра или ловит исключения, но это не мои проблемы.

Курильщик №2 делает RunspacePooll


Тут тоже делаем поле IsDisposed, но на этот раз делаем его с публичным гетером, чтобы человеку, использующему библиотеку не пришлось писать больше защитного кода.
class RunspacePoolInTheNuttshell() : IDisposable
{
    public static bool IsDisposed = false;
    
    public void Dispose()
    {
        if (IsDisposed) return;
        IsDisposed = true;
        GC.SuppressFinalize(this);
        Console.WriteLine("I was invoked");
    }
}

Если метод Dispose был вызван, делаем return и дело с концом. Конечно, при повторном обращении к полю он получит nullref, потому что объект уже будет удален из памяти, но моя ли это проблема.

Здоровый человек использует библиотеку:


Здесь исключение, здесь не исключение, здесь рыбу заворачиваем. Оба класса идут в одном пакете и имеют различное поведение. Классы выбрасывают один и тот же тип исключений по разным причинам.
  • Неправильный пароль? InvalidRunspacePoolStateException!
  • Нет соединения? InvalidRunspacePoolStateException!

Получается, что в одном месте нужно обработать ObjectDisposedException, в другом NullReferenceException в третьем InvalidRunspacePoolStateException и все полно сюрпризов.

Исключение — не решение


fqkmkc_yinnmnmtqwlqyatazwzi.png

До причащения святых таинств я читал файл по-старому:
public static void Main()
{
    string txt = @"c:\temp\test.txt";
    
    if (File.Exists(txt)) return;
    string readText = File.ReadAllText(txt);
    Console.WriteLine(readText);
}

Но после просмотра видео я начал делать по-новому:
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");
    }
}

Или это по-новому?
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);
}

Как именно по-новому? Где именно кончается ответственность разработчика и начинается ответственность пользователя?
6fpnxmjzipdpjdsqlqvf3nkchye.png

В целом, задумка ясна, если ты являешься автором библиотеки, то можно сразу вызывать метод и подавать на него некорректные данные, ты прекрасно знаешь предметную область и описал все случаи неправильного поведения, выбросил исключения, которые имеют смысл и сам их обработал.
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("Компенсаторная логика");
}

Самые догадливые уже поняли, к чему я веду. Организация коррекции ошибок построенная на try catch блоках приведет только к углублению вложенности кода.

Используя этот паттерн мы в любом случае отказываемся от исполнения кода, но вежливее.

В целом, ничего нового, еще 10 лет назад люди начали подозревать что С# перегружен паттернами и из года в год их меньше не становится. Встречайте еще один, бросать исключения стало еще проще.

И под конец — операторы


hiia8jny2hp6ejrb7jyxlsmwyli.png

Операторы не должны ничего никуда кастить.

Пример из JS вы наверняка знаете:

console.log('2'+'2'-'2');
// 20
 
Дизайнеры JS посчитали, что отдельный оператор сложения и отдельный оператор конкатенации не нужны, поэтому делать математику на JS небезопасно.

Источником этого бага в JS является неявное приведение типа string к типу int посредством оператора. Так лишний сахар становится багом.

Неявным кастом типов болеет и C# тоже, хоть и гораздо реже. Взять, например, пользовательский инпут, который после обновления библиотеки начал мапиться в string вместо int, как раньше, а оператор (+) и математический оператор и оператор конкатенации. 

Изменение типа с int на string код не сломало, а бизнес-логику сломало.

Так что оставлю это здесь, а вы попытайтесь без запуска угадать результат выполнения. 

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

oug5kh6sjydt9llengsiebnp40w.png

© Habrahabr.ru