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/
Добавить комментарий