Русский
Русский
English
Статистика
Реклама

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



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а в недоступном _runspacee вызовет исключение.

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

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

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


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

Источник: habr.com
К списку статей
Опубликовано: 15.04.2021 16:11:54
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Блог компании ruvds.com

Программирование

C

Паттерны

Ruvds_статьи

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru