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

от автора

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

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

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

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

Возьмем плохо написанный фрагмент кода, который написал любитель на коленке. Этот метод проверяет состояние экземпляра класса и возвращает 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 и все полно сюрпризов.

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

До причащения святых таинств я читал файл по-старому:

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

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

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

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 лет назад люди начали подозревать что С# перегружен паттернами и из года в год их меньше не становится. Встречайте еще один, бросать исключения стало еще проще.

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

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

Пример из 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' }");     } }

ссылка на оригинал статьи https://habr.com/ru/company/ruvds/blog/552584/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *