«Пишите код по-новому (тм)»
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 и все полно сюрпризов.
Исключение — не решение
До причащения святых таинств я читал файл по-старому:
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' }");
}
}