Код живой и мёртвый. Часть третья. Код как текст

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


    В прошлых двух статьях я показал, что тщательно выбранные слова помогают лучше понимать суть написанного, но думать только о них недостаточно, ведь всякое слово существует в двух формах: как само по себе и как часть предложения. Повтор CurrentThread ещё не повтор, пока мы не читаем его в контексте Thread.CurrentThread.


    Таким образом, ориентируясь в нотах и простых мелодиях, мы посмотрим теперь, что такое музыка.


    Оглавление цикла


    1. Объекты
    2. Действия и свойства
    3. Код как текст

    Код как текст


    Большинство fluent-интерфейсов разрабатываются с упором на внешнее, а не внутреннее, поэтому их так легко читать. Разумеется, не бесплатно: содержание в некотором смысле ослабевает. Так, скажем, в пакете FluentAssertions можно написать: (2 + 2).Should().Be(4, because: "2 + 2 is 4!"), и, относительно чтения, because смотрится элегантно, но внутри метода Be() ожидается, скорее, параметр error или errorMessage.


    На мой взгляд, такие послабления несущественны. Когда мы соглашаемся, что код — это текст, его составляющие перестают принадлежать сами себе: они теперь часть некоего всеобщего "Эфира".


    Покажу на примерах, как такие соображения становятся опытом.


    Interlocked


    Напомню случай с Interlocked, который мы из Interlocked.CompareExchange(ref x, newX, oldX) превратили в Atomically.Change(ref x, from: oldX, to: newX), используя понятные имена методов и параметров.


    ExceptWith


    У типа ISet<> есть метод, который называется ExceptWith. Если посмотреть на вызов вроде items.ExceptWith(other), не сразу сообразишь, что происходит. Но стоит только написать: items.Exclude(other), как всё становится на свои места.


    GetValueOrDefault


    При работе с Nullable<T> обращение к x.Value бросит исключение, если в x находится null. Если получить Value всё-таки нужно, используется x.GetValueOrDefault: это или Value, или значение по умолчанию. Громоздко.


    Выражению "или x, или значение по умолчанию" сооветствует короткое и изящное x.OrDefault.


    int? x = null;
    
    var a = x.GetValueOrDefault(); // Сложное, большущее выражение. Не годится.
    var b = x.OrDefault();         // Простое — как пишется, так и читается.
    var c = x.Or(10);              // А можно ещё вот как.

    С OrDefault и Or есть одно но, которое стоит помнить: при работе с оператором .? нельзя написать нечто вроде x?.IsEnabled.Or(false), только (x?.IsEnabled).Or(false) (проще говоря, оператор .? отменяет всю правую часть, если в левой null).


    Шаблон можно применить при работе с IEnumerable<T>:


    IEnumerable<int> numbers = null;
    
    // Многословно.
    var x = numbers ?? Enumerable.Empty<int>();
    
    // Коротко и изящно.
    var x = numbers.OrEmpty();

    Math.Min и Math.Max


    Идею с Or можно развить на числовые типы. Положим, требуется взять максимальное число из a и b. Тогда мы пишем: Math.Max(a, b) или a > b ? a : b. Оба варианта выглядят достаточно привычно, но, тем не менее, не похожи на естественный язык.


    Заменить можно на: a.Or(b).IfLess()взять a или b, если a меньше. Подходит для таких ситуаций:


    Creature creature = ...;
    int damage = ...;
    
    // Обычно пишется так.
    creature.Health = Math.Max(creature.Health - damage, 0);
    
    // Fluent.
    creature.Health = (creature.Health - damage).Or(0).IfGreater();
    
    // Но ещё сильнее:
    creature.Health = (creature.Health - damage).ButNotLess(than: 0);

    string.Join


    Иногда нужно последовательность собрать в строку, разделяя элементы пробелом или запятой. Для этого используется string.Join, например, так: string.Join(", ", new [] { 1, 2, 3 }); // Получим "1, 2, 3"..


    Простое "Раздели числа запятой" может стать вдруг "Присоедини запятую к каждому числу из списка" — это уж точно не код как текст.


    var numbers = new [] { 1, 2, 3 };
    
    // "Присоединяем" запятую к числам — не звучит.
    var x = string.Join(", ", numbers);
    
    // Разделяем числа запятой — интуитивно!
    var x = numbers.Separated(with: ", "); 

    Regex


    Впрочем, string.Join вполне безобиден по сравнению с тем, как подчас неверно и не по назначению используется Regex. Там, где можно обойтись простым читаемым текстом, почему-то предпочитается переусложнённая запись.


    Начнём с простого — определения, что строка представляет набор цифр:


    string id = ...;
    
     // Коротко, но избыточно.
    var x = Regex.IsMatch(id, "^[0-9]*$");
    
    // Сильнее.
    var x = id.All(x => x.IsDigit());
    
    // Идеально!
    var x = id.IsNumer();                  

    Другой случай — узнаём, есть ли в строке хоть один символ из последовательности:


    string text = ...;
    
    // Сумбурно и путано.
    var x = Regex.IsMatch(text, @"["<>[]'");
    
    // Коротко и ясно. (И быстрее.)
    var x = text.ContainsAnyOf('"', '<', '>', '[', ']', '\'');
    // Или так.
    var x = text.ContainsAny(charOf: @"["<>[]'");

    Чем сложнее задача, тем сложнее "узор" решения: чтобы разбить запись вида "HelloWorld" на несколько слов "Hello World", кому-то вместо простого алгоритма захотелось монстра:


    string text = ...;
    
    // Даже с онлайн-калькулятором не совсем понятно.
    var x = Regex.Replace(text, "([a-z](?=[A-Z])|[A-Z](?=[A-Z][a-z]))", "$1 ");
    
    // Теперь понятно.
    var x = text.PascalCaseWords().Separated(with: " ");
    
    // Так тоже хорошо.
    var x = text.AsWords(eachStartsWith: x => x.IsUpper()).Separated(with: " ");

    Бесспорно, регулярные выражения эффективны и универсальны, но хочется понимать происходящее с первого взгляда.


    Substring и Remove


    Бывает, нужно удалить из строки какую-нибудь часть с начала или конца, например, из path — расширение .txt, если оно есть.


    string path = ...;
    
    // Классический подход в лоб.
    var x = path.EndsWith(".txt") ? path.Remove(path.Length - "txt".Length) : path;
    
    // Понятный метод расширения.
    var x = path.Without(".exe").AtEnd;

    Снова действие и алгоритм ушли, и осталась простая строка без расширения .exe в конце.


    Поскольку метод Without должен возвращать некий WithoutExpression, напрашиваются ещё: path.Without("_").AtStart и path.Without("Something").Anywhere. Интересно ещё, что с таким же словом можно построить другое выражение: name.Without(charAt: 1) — удаляет символ по индексу 1 и возвращает новую строку (полезно при вычислении перестановок). И тоже читаемо!


    Type.GetMethods


    Чтобы получить методы определённого типа с помощью рефлексии, используют:


    Type type = ...;
    
    // Тут и `Get` лишний, и оператор `|`. Ни к чему такие сложности.
    var x = type.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
    
    // Хорошая, понятная замена. `Or` ничего не делает, декорация.
    var x = type.Methods(_ => _.Instance.Public.Or.NonPublic);

    (То же самое подходит и для GetFields и GetProperties.)


    Directory.Copy


    Всякие операции по работе с папками и файлами частенько обобщаются до DirectoryUtils, FileSystemHelper. Там реализуют обход файловой системы, очистку, копирование и т.д. Но и тут можно придумать кое-что получше!


    Отображаем текст "скопировать все файлы из 'D:\Source' в 'D:\Target'" на код "D:\\Source".AsDirectory().Copy().Files.To("D:\\Target"). AsDirectory() — возвращает DirectoryInfo из string, а Copy() — создаёт экземпляр CopyExpression, описывающий однозначный API для построения выражений (нельзя вызвать Copy().Files.Files, например). Тогда открываются возможности копировать не все файлы, а некоторые: Copy().Files.Where(x => x.IsNotEmpty).


    GetOrderById


    Во второй статье я писал, что IUsersRepository.GetUser(int id) — избыточно, и лучше — IUsersRepository.User(int id). Соответственно, в аналогичном IOrdersRepository мы имеем не GetOrderById(int id), а Order(int id). Тем не менее, в другом примере предлагалось переменную такого репозитория называть не _ordersRepository, а просто _orders.


    Оба изменения хороши сами по себе, но вместе, в контексте чтения, не складываются: вызов _orders.Order(id) смотрится многословно. Можно было бы _orders.Get(id), но у заказов ничего не получается, мы только хотим указать тот, который имеет такой идентификатор. "Тот, который" — это One, поэтому:


    IOrdersRepository orders = ...;
    int id = ...;
    
    // Классика с излишествами.
    var x = orders.GetOrderById(id);
    
    // Вторая статья цикла говорит писать так:
    var x = orders.Order(id);
    
    // Но мы и так понимаем, что работаем с заказами.
    var x = orders.One(id);
    
    // Или с именованым параметром:
    var x = orders.One(with: id);

    GetOrders


    В таких объектах, как IOrdersRepository, часто встречаются и другие методы: AddOrder, RemoveOrder, GetOrders. Из первых двух повторения уходят, и получаются Add и Remove (с соответствующими записями _orders.Add(order) и _orders.Remove(order)). С GetOrders сложнее — переименовать на Orders мало. Давайте посмотрим:


    IOrdersRepository orders = ...;
    
    // Совсем не подходит.
    var x = orders.GetOrders();
    
    // Без `Get`, но глупость.
    var x = orders.Orders();
    
    // Эврика!
    var x = orders.All();

    Нужно заметить, что при старом _ordersRepository повторения в вызовах GetOrders или GetOrderById не так заметны, ведь работаем-то с репозиторием!


    Имена вроде One, All подходят для многих интерфейсов, представляющих множества. Скажем, в известной реализации GitHub API — octokit — получение всех репозиториев пользователя выглядит как gitHub.Repository.GetAllForUser("John"), хотя логичнее — gitHub.Users.One("John").Repositories.All. При этом получение одного репозитория будет, соответственно, gitHub.Repository.Get("John", "Repo") вместо очевидного gitHub.Users.One("John").Repositories.One("Repo"). Второй случай выглядит длиннее, но он внутренне согласован и отражает платформу. К тому же, с помощью методов расширения его можно сократить до gitHub.User("John").Repository("Repo").


    Dictionary.TryGetValue


    Получение значений из словаря делится на несколько сценариев, которые отличаются только тем, что нужно делать, если ключ не найден:


    • бросить ошибку (dictionary[key]);
    • вернуть значение по умолчанию (не реализовано, но часто пишут GetValueOrDefault или TryGetValue);
    • вернуть что-то другое (не реализовано, но я бы ожидал GetValueOrOther);
    • записать указанное значение в словарь и вернуть его (не реализовано, но встречается GetOrAdd).

    Выражения сходятся в точке "берём какой-то X, или Y, если X нет". Кроме этого, как и в случае с _ordersRepository, переменную словаря мы назовём не itemsDictionary, а items.


    Тогда для части "берём какой-то X" идеально подходит вызов вида items.One(withKey: X), возвращающий структуру с четырьмя концовками:


    Dictionary<int, Item> items = ...;
    int id = ...;
    
    // Как правило, значения получаются так:
    var x = items.GetValueOrDefault(id);
    var x = items[id];
    var x = items.GetOrAdd(id, () => new Item());
    
    // Но проще и согласованней:
    var x = items.One(with: id).OrDefault();
    var x = items.One(with: id).Or(Item.Empty);
    var x = items.One(with: id).OrThrow(withMessage: $"Couldn't find item with '{id}' id.");
    var x = items.One(with: id).OrNew(() => new Item());

    Assembly.GetTypes


    Посмотрим на создание всех существующих в сборке экземпляров типа T:


    // Классика.
    var x = Assembly
        .GetAssembly(typeof(T))
        .GetTypes()
        .Where(...)
        .Select(Activator.CreateInstance);
    
    // "Плохая" декомпозиция.
    var x = TypesHelper.GetAllInstancesOf<T>();
    
    // Выразительнее.
    var x = Instances.Of<T>();

    Таким образом, иногда, имя статического класса — начало выражения.


    Нечто похожее можно встретить в NUnit: Assert.That(2 + 2, Is.EqualTo(4))Is и не задумывался как самодостаточный тип.


    Argument.ThrowIfNull


    Теперь взглянем на проверку предусловий:


    // Классические варианты.
    Argument.ThrowIfNull(x);
    Guard.CheckAgainstNull(x);
    
    // Описательно.
    x.Should().BeNotNull();
    
    // Интересно, но невозможно... Или возможно?
    Ensure(that: x).NotNull();

    Ensure.NotNull(argument) — симпатично, но не совсем по-английски. Другое дело написанное выше Ensure(that: x).NotNull(). Если бы только там можно было...


    Кстати, можно! Пишем Contract.Ensure(that: argument).IsNotNull() и импортируем тип Contract с помощью using static. Так получаются всякие Ensure(that: type).Implements<T>(), Ensure(that: number).InRange(from: 5, to: 10) и т.д.


    Идея статического импорта открывает множество дверей. Красивого примера ради: вместо items.Remove(x) писать Remove(x, from: items). Но любопытнее сокращение перечислений (enum) и свойств, возвращающих функции.


    IItems items = ...;
    
    // Неплохо.
    var x = items.All(where: x => x.IsWeapon);
    
    // Пока хуже.
    // `ItemsThatAre.Weapons` возвращает `Predicate<bool>`.
    var x = items.All(ItemsThatAre.Weapons);
    
    // `using static` всё выровнял! Читается прекрасно.
    var x = items.All(Weapons);

    Экзотический Find


    В С# 7.1 и выше можно писать не Find(1, @in: items), а Find(1, in items), где Find определяется как Find<T>(T item, in IEnumerable<T> items). Этот пример непрактичен, но показывает, что все средства хороши в борьбе за читаемость.


    Итого


    В этой части я рассмотрел несколько способов работать с читаемостью кода. Все их можно обобщить до:


    • Именованный параметр как часть выраженияShould().Be(4, because: ""), Atomically.Change(ref x, from: oldX, to: newX).
    • Простое имя вместо технических деталейSeparated(with: ", "), Exclude.
    • Метод как часть переменнойx.OrDefault(), x.Or(b).IfLess(), orders.One(with: id), orders.All.
    • Метод как часть выраженияpath.Without(".exe").AtEnd.
    • Тип как часть выраженияInstances.Of, Is.EqualTo.
    • Метод как часть выражения (using static)Ensure(that: x), items.All(Weapons).

    Так выводится на первый план внешнее и созерцаемое. Сперва мыслится оно, а уж затем мыслятся его конкретные воплощения, уже не столь значительные, покуда код читается как текст. Из этого следует, что судьёй будет не столько вкус, сколько язык — он определяет разницу между item.GetValueOrDefault и item.OrDefault.


    Эпилог


    Что лучше, понятный, но нерабочий метод, или рабочий, но непонятный? Белоснежный замок без мебели и комнат или сарай с диванами в стиле Людовика XIV? Роскошная яхта без двигателя или кряхтящая баржа с квантовым компьютером, которым никто не умеет пользоваться?


    Полярные ответы не подходят, но и "где-то посередине" — тоже.


    На мой взгляд, оба понятия неразрывны: тщательно выбирая обложку для книги, мы с сомнением поглядываем на ошибки в тексте, и наоборот. Я бы не хотел, чтобы Beatles играли некачественную музыку, но и чтобы назывались они MusicHelper — тоже.


    Другое дело, что работа над словом как часть процесса разработки — дело недооценённое, непривычное, и поэтому какая-то крайность в суждениях всё-таки нужна. Этот цикл — крайность формы и картинки.


    Всем спасибо за внимание!


    Ссылки


    Кому интересно посмотреть ещё примеры, их можно найти у меня на GitHub, например, в библиотеке Pocket.Common. (не для всемирного и повсеместного использования)

    Поделиться публикацией

    Комментарии 102

      +3
      var a = x.GetValueOrDefault(); // Сложное, большущее выражение. Не годится.
      var b = x.OrDefault(); // Простое — как пишется, так и читается.
      var c = x.Or(10); // А можно ещё вот как.

      как по мне, первый вариант GetValueOrDefault более читабельный, можно сократить до GetOrDefault. На хешмапах начиная java8 так и сделали кстати
        0
        можно сократить до GetOrDefault

        А потом, следуя советам второй статьи, мы убираем Get, и получается OrDefault — лаконично и просто, без излишеств.

          +2
          У хешмапов есть еще putIfAbsent, put тоже будем убирать? :)
          Неее, именно .GetOrDefault(key, 0) отлично ложится и читается
            0

            Put не будем, потому что это действие, и его мы хотим отразить.
            Get будем, потому что это не действие, и его мы не хотим отражать.

              +1
              Get будем, потому что это не действие
              Речь про изменение состояния?
                +1

                Не совсем понимаю, какое состояние меняется.


                Вот есть словарь, а вот есть значение с ключом x. Что получается и что меняется?

                  0
                  Get это тоже глагол как и Put и это тоже действие. Но это действие не изменяет состояние/содержимое хэшмапа. Я и уточнил, что подразумевается под «это не действие»
                    +1
                    Я и уточнил, что подразумевается под «это не действие»

                    Я подразумевал такую штуку: если нужно показать действие (Put), то мы используем глагол; если нужно показать просто какую-то вещь как часть другой — не используем.


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


                    Знаю, в Java всё равно пишут getWeight(), но, мне кажется, это кто-то зря придумал и очень-очень давно. Лучше weight(). Потому что вес — это не действие, которое яблоко умеет делать, а его свойство.

                      0
                      Почему «положить в» — это действие, а «вытащить из» — уже не действие?
                        0

                        Потому что "вытащить из" — промежуточный этап, который не так важен, как его результат.


                        Когда я кладу элемент в коллекцию Put, мне жизненно необходимо понимать, что нечто случится. Действие — конечная цель.
                        Когда мне нужен элемент коллекции по ключу, то не имеет значения, достаётся он, получается, и т.д. Даже если он достаётся из базы, он нужен всё равно. Элемент — конечная цель.

                0
                В .NET есть совершенно четкая разница между свойством (без префикса Get, ессно) и методом: свойство выполняется мгновенно, а выполнение метода может быть длительным. Если разработчик видит Get, он сразу понимает, что полученное значение желательно сохранить в локальной переменной. GetValueOrDefault потенциально может быть длительным, так как неизвестно, сколько этот самый Default будет создаваться, поэтому Get опускать нельзя.
                  –1

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

                    +2
                    Names of Methods
                    Because methods are the means of taking action, the design guidelines require that method names be verbs or verb phrases. Following this guideline also serves to distinguish method names from property and type names, which are noun or adjective phrases.

                    ✓ DO give methods names that are verbs or verb phrases.
                      0

                      Рекомендация сама по себе неплохая, но её слабость в следующем утверждении:


                      Because methods are the means of taking action

                      Это неверно.


                      Поэтому мы видим:


                      • Where, Select и т.д.;
                      • Enumerable.Empty<T>();
                      • ImmutableArray.As<T>().
                      • И т.д.
                        0
                        Where, Select выполняются мгновенно, так как вычисления LINQ-выражений ленивые;

                        Enumerable.Empty() возвращает константу;

                        ImmutableArray.As() — здесь итак понятно, что время выполнения прямо пропорционально размеру массива;

                        В любом случае, чтобы нарушить рекомендации нужны веские основания.
              +1
              А потом, следуя советам второй статьи, мы убираем Get

              Не убираем, потому что становится совсем не очевидно что функция вообще делает без предварительного ее просмотра. Избыточность не всегда плохо.
            0
            Для сопровождения программы код приходится читать, и тем это делать проще, чем больше он похож на естественный язык, — тогда быстрее вникаешь и сосредотачиваешься на главном.

            Вам бы попробовать AppleScript – возможно, мнение бы изменилось.
            Максимум сходства с естественным языком… И это совсем не радует.
              0

              Максимум сходства с естественным языком обеспечивает разработчик. Язык программирования предоставляет для этого средства: лаконичный синтаксис, методы расширений, UFCS, шаблоны и макросы и т.д.


              Что пользоваться этим всем можно не совсем элегантно — тоже верно.

              +4
              Выражению «или x, или значение по умолчанию» сооветствует короткое и изящное x.OrDefault.

              int? x = null;
              
              var a = x.GetValueOrDefault(); // Сложное, большущее выражение. Не годится.
              var b = x.OrDefault();         // Простое — как пишется, так и читается.
              var c = x.Or(10);              // А можно ещё вот как.
              

              Именно поэтому разработкой библиотек и фреймворков должны заниматься прожжёные профессионалы. Вот как Ваш код выглядит со стороны.

              int? x = null;
              
              int a = x.GetValueOrDefault(); // Всё понятно, отлично!
              int? b = x.OrDefault();        // Здесь должен быть null, не?
              int c = x.Or(10);              // int? | 10 Чё за хрень?!
              
                –1

                На мой взгляд, вы используете один и тот же критерий для оценки двух совершенно различных подходов к написанию кода. Так можно и про 5.June() сказать: у числа не может быть июня или а что если 91.June() вызвать.


                var a = x.GetValueOrDefault()"взять значение у x или значение по умолчанию (семантика Nullable выводится из контекста вызова) и поместить его в переменную a".


                var a = x.OrDefault"a — это или x, или значение по умолчанию (семантика Nullable так же выводится)".


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


                Дополню примером из Python, где синтаксис уже предполагает семантику or:


                for item in items or []
                  +2
                  Вы понимете разницу между default(int) и default(int?). Вы понимаете, что x.GetValueOrDefault() и x.OrDefault() это совсем не одно и то же?

                  Ко второму нужно привыкнуть

                  Нельзя заставлять миллионы разработчиков привыкать, им нужен четкий и понятный API.
                    0
                    Вы понимете разницу между default(int) и default(int?). Вы понимаете, что x.GetValueOrDefault() и x.OrDefault() это совсем не одно и то же?

                    Одно и то же. Вы же не пишете int? numberOrNull, а пишете int? number (что, по вашей логике, неверно, поскольку там не число, а число или ничего). Некоторые детали отдаются типу и контексту, так что тут, на мой взгляд, всё в порядке.


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


                    Нельзя заставлять миллионы разработчиков привыкать, им нужен четкий и понятный API.

                    Боюсь, я этого не утверждал...

                +1
                Есть довольно устоявшиеся в шарпе First & Single. Какая семантика у вашего предложенного One?

                Некоторые сокращения смущают, ибо нет понимания, что же они сделают. Меньше слов — не значит лучше. Например orders.All(); сам по себе — невнятен. Это может быть All от Linq (возвращает bool), может быть All с типом IQueryable (и я могу достроить выражение), а может быть что-то ещё. Читаю это примерно как «заказы всё», что «всё» — да просто всё, кончились. Угадывай, что там автор апи имел в виду.

                UPD: для nullable есть простая старая конструкция:
                int? x = null;
                int value = x ?? 10;
                  0
                  заказы всё

                  Корректно: заказы все. Это значит — все заказы. Что это не вызов LINQ выведется из контекста.


                  UPD: для nullable есть простая старая конструкция:
                  int? x = null;
                  int value = x ?? 10;

                  Она тоже неплоха и лаконична, хотя и не так читаема, как x.Or(10).

                    0
                    Рассчитывать на контекст — плохая идея. Апи должно быть читабельным даже без него.

                    Кстати, что скажете насчет File.ReadLines и File.ReadAllLines? У меня к этим методам давний вопрос, как можно было сделать апи максимально неочевидным =)
                      0
                      Рассчитывать на контекст — плохая идея. Апи должно быть читабельным даже без него.

                      На мой взгляд, напротив: нужно всегда помнить, что у каждого вызова есть контекст, например, методы вызываются или в контексте имени переменной, или в контексте имени класса (если метод статический). Иначе получится Directory.CreateDirectory.


                      Проектируя методы с опорой на контекст, например, на определённую форму слова переменной, мы тем самым ещё и заставляем разработчика избегать всяческих двусмысленностей и коверканий: One лучше подходит к users, чем к usersRepository.


                      Кстати, что скажете насчет File.ReadLines и File.ReadAllLines? У меня к этим методам давний вопрос, как можно было сделать апи максимально неочевидным =)

                      И не подозревал о существовании File.ReadLines. Да уж, выглядит, конечно, не очень (с точки зрения API). Но я бы не хотел видеть File.EnumerateAllLines, которое, уверен, первым приходит в голову как хорошее решение.


                      Понимая, что тут очень важно показать разницу между ленивым считыванием строчек из файла и неленивым, я бы, вероятно, написал нечто такое:


                      public class FileLines
                      {
                          public static FileLines Of(string path) =>
                                    new FileLines(path);
                      
                          private readonly string _path;
                      
                          private FileLines(string path) =>
                              _path = path;
                      
                          public IEnumerable<string> AsEnumerable() =>
                              File.ReadLines(_path);
                      
                          public string[] AsArray() =>
                              File.ReadAllLines(_path);
                      }
                  +1
                  Теперь не хватает только реализации всего этого.
                    0

                    Часть этих штук можно посмотреть у меня на GitHub.

                    +1
                    Если посмотреть на вызов вроде items.ExceptWith(other), не сразу сообразишь, что происходит. Но стоит только написать: items.Exclude(other), как всё становится на свои места.

                    Нет, не становится: items.ExceptWith(other) на взгляд означает: вернуть items за исключением other, тогда как items.Exclude(other) на взгляд означает: исключить other из items (т. е. изменить items).

                      +1
                      items.ExceptWith(other) на взгляд означает: вернуть items за исключением other,

                      Проблема в том, что ExceptWith ничего не возвращает.

                        +1
                        Упс, а должен был бы.
                      0
                      string path = ...;
                      
                      // Классический подход в лоб.
                      var x = path.EndsWith(".txt") ? path.Remove(path.Length - "txt".Length) : path;
                      
                      // Понятный метод расширения.
                      var x = path.Without(".exe").AtEnd;
                      

                      // Реальный классический подход
                      string path = ...;
                      Path.GetFileNameWithoutExtension(path);
                      

                      Великолепно! А что если я захочу сделать так:
                      var x = path.Without(".exe"); // x это что? Я ожидаю, что это строка без всех вхождений ".exe", так ли это? И чем это отличется от path.Replace(".exe", "")?
                      
                        0
                        // Реальный классический подход
                        string path = ...;
                        Path.GetFileNameWithoutExtension(path);

                        path.Without(x).AtEnd определён для задач, предполагающих работу не только с расширениями файлов.


                        var x = path.Without(".exe"); // x это что? Я ожидаю, что это строка без всех вхождений ".exe", так ли это? И чем это отличется от path.Replace(".exe", "")?

                        К сожалению, C# не позволяет определить path.Without(".exe") так, чтобы он имел смысл, если не было вызвано никакое из продолжений вроде End. Тем не менее, это решается с помощью path.Without(".exe").Everywhere.

                        +1
                        Заменить можно на: a.Or(b).IfLess() — взять a или b, если a меньше.

                        Опять нет, читается как: a или b, если меньше. Что именно меньше?
                          0

                          Тут с вами соглашусь! Пожалуй, действительно, слабый момент. Я очень долго думал над тем, как избежать неоднозначности в таких случаях, когда мы ограничиваем какое-то значение нижним или верхним пределом (здоровье не может опуститься ниже нуля).


                          Улучшить, вероятно, стоит так:


                          Creature creature = ...;
                          int damage = ...;
                          
                          // Было.
                          creature.Health = (creature.Health - damage).Or(0).IfLess();
                          
                          // Стало.
                          creature.Health = (creature.Health - damage).ButNotLess(than: 0);
                            0
                            За инвариантом Health >= 0 должен следить класс Creature, это его зона ответственности

                            class Creature
                            {
                                public int Health { get; private set; }
                            
                                public void Hit(int damage) =>
                                     Health = damage > Health
                                        ? 0
                                        : Health - damage;
                            }
                            
                              0

                              Это находится за пределами темы статьи и обсуждения.

                              +1
                              theCreature.Health = (theCreature.Health - damage).ButNotLess(than: 0).However().IfAndOnlyIf(theCreature is (Terribly)Lucky).Then(theCreature.Health + 2 * damage);

                                –1

                                Не уверен, иронизируете вы или нет, но код действительно понятный и читаемый (хотя и непрактичный). Лучше в десятки раз, чем любые Utils.

                            +1
                            Стойкое ощущение что автор предлагает тараканов из голов одних разработчиков (которые предложили те или иные подходы) заменить на тараканов из своей головы. Но беда в том, что тараканы от этого не перестают быть тараканами (что прекрасно подтверждают комментарии к статье).
                            Можно было бы тоже начать критиковать конкретные предложения из статьи (а многое из предложенных конкретных примеров и правда — очень плохо), но в этом нет никакого прикладного смысла. Ибо изначальная-то идея совершенно здравая — много думать над понятностью кода. А вот с реализацией — как в той картинке с программистом «Теперь-то я сделаю всё правильно!». Но это объясняется тем, что сложность, в целом, никуда нельзя убрать из индустрии разработки ПО. Это действительно сложно.
                              +1
                              Чувствую себя сукой, но я просто вначле читаю Ваш код, а уже затем комментарии к нему, чесслово.

                              // Теперь понятно.
                              var x = text.PascalCaseWords().Separated(with: " ");
                              

                              Мне понятно так: выбрать все слова в Паскалевском стиле и объеденить их пробелами.
                                0
                                Мне понятно так: выбрать все слова в Паскалевском стиле и объеденить их пробелами.

                                Вы всё верно поняли.

                                  0
                                  В моем понимании «HelloWorld».PascalCaseWords().Separated(with: " "); вернет «HelloWorld», а у Вас он должен вернуть «Hello World».
                                    +1
                                    Во всем этом семантичном и текстовом коде есть главная проблема. Программисты все сплошь в лучшем случае б2 (конечно, за исключениями), и поскольку кроме как на галеры английский язык на собесах не спрашивают (да и там не парятся), получаются вот эти перлы типа separated with вместо by.
                                    Вот мы например по этой причине отказались от английского геркина в bdd и перешли на русский.
                                      0
                                      Программисты все сплошь в лучшем случае б2

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


                                      получаются вот эти перлы типа separated with вместо by

                                      Это несущественно, но точнее, если не ошибаюсь, как раз with, а не by. Пожалуйста.

                                        0
                                        Туше. Посыпаю голову пеплом и иду левелапать свой б2 =)
                                  0
                                  // `using static` всё выровнял! Читается прекрасно.
                                  var x = items.All(Weapons);
                                  

                                  Учитывая, что System.Linq.IEnumerable имеет аналогичный метод, читается как… ну, Вы поняли?
                                    –1
                                    Учитывая, что System.Linq.IEnumerable имеет аналогичный метод, читается как… ну, Вы поняли?

                                    Согласен, двусмысленность есть. Но она — в примере, в демонстрации, общий принцип остаётся таким же: использовать using static, чтобы упрощать понимание и читаемость некоторых участков кода.


                                    Приведу другой:


                                    public enum JobStatus
                                    {
                                        None = 0,
                                        Running = 1,
                                        Completed = 2,
                                        Cancelled = 3,
                                        Failed = 4,
                                    }
                                    
                                    public class Job
                                    {
                                        public JobStatus Status { get; }
                                    
                                        public bool Is(JobStatus status) => Status == status;
                                    }
                                    
                                    // Как вызывать метод `Is`?
                                    // Как обычно.
                                    job.Is(JobStatus.Running);
                                    
                                    // С помощью `using static`.
                                    job.Is(Running); 

                                    Разумеется, у using static есть свои нюансы (нужно, чтобы не было коллизий в именах), но, тем не менее, это инструмент!

                                    –1
                                    Тема Лисп-макросов не раскрыта :)
                                    Не, к C# они отношения не имеют, но это пример того, как в других языках есть средства создания «кода, похожего на естественный язык».
                                    В отношении лиспа много где обсуждаются достоинства и недостатки такого подхода. Главное достоинство — программист пишет так, как ему удобно думать. Главный недостаток — его коллегам удобно думать по-другому. В итоге шаблонный код оказывается в среднем более понятным и поддерживаемым, чем изящный и литературный.
                                    var numbers = new [] { 1, 2, 3 };
                                    
                                    // "Присоединяем" запятую к числам — не звучит.
                                    var x = string.Join(", ", numbers);
                                    
                                    // Разделяем числа запятой — интуитивно!
                                    var x = numbers.Separated(with: ", "); 

                                    ;; Pure beauty!!!
                                    (join numbers :into 'string :having-in-between ", ")
                                    
                                      0
                                      Главное достоинство — программист пишет так, как ему удобно думать. Главный недостаток — его коллегам удобно думать по-другому.

                                      Поэтому, как мне видится, в качестве судьи нужно использовать язык. Например, string.Join(", ", numbers) можно прочитать разве что как "присоединить к строке запятую и числа", но разве так можно сказать? Да и зачем, если уже есть выражение "separate with a comma"?


                                      Разумеется, о том, какие предложения языка считать более понятными, а какие менее — можно отдельно спорить. Но хорошо бы хотя бы начать вести такие споры. Ведь суть того же примера со string.Join не в самом string.Join, а в том, что над словом нужно думать, искать, подбирать (что также демонстрирует пример с ExceptWith). А то подчас простой по содержанию код похож на философский трактат с его интенциями, трансцендентностями и прочим.

                                        +1

                                        Вот хоть убейте, Separate должен возвращать нечто разделенное (аналог string.Split). А у вас возвращается строка, склеенная из частей. Если бы я разбирался в АПИ незнакомой библиотеки, я искал бы concat, combine, join, glue, aggregate или что-то в этом роде. И когда, не найдя, я полез бы в справку и нашел там, что мне надо использовать separate, я подумал бы, что кто-то сошел с ума — или я, или автор библиотеки.
                                        Особенно выразительно смотрится с пустым разделителем. chars.separate(""). Awesome

                                          0
                                          Кстати, вот если язык брать как критерий првильности, то как правильно говорить: список, разделенный запятыми, или список, объединенный запятыми?
                                            0
                                            Когда на входе — коллекция, а на выходе — строка, то это-таки «элементы, соединенные запятыми». Если же наоборот — то можно получить, допустим, список слов, разделенных [в исходной строке] пробелами.
                                              0
                                              Так, ясно, Join — это глагол, Separated — это прилагательное, первое — это действие, второе — это результат. Оба варианта правильны (включая вырожденные случаи с пустым разделителем). Имеет ли смысл заменять императивную семантику на декларативную? Я без понятия. Учитывая, что весь .NET написан в императивном стиле, я бы публичные сборки делал так же, а для внутренних нужд можно и декларативно. Главное, чтобы было понятно и однозначно.
                                                +1
                                                Join — это глагол, Separated — это прилагательное, первое — это действие, второе — это результат.

                                                Даже если оба слова сделать активными (join, separate) или пассивными глаголами (joined, separated), суть не изменяется. Первый про объединение, второй про разделение, и действия эти направлены противоположно. Увидеть separated там, где ожидаешь увидеть joined, не ложится в прицнип наименьшего удивления.
                                                  –1
                                                  Зна́ки препина́ния — элементы письменности, выполняющие вспомогательные функции разделения (выделения) смысловых отрезков текста, предложений, словосочетаний, слов, частей слова, указания на грамматические и логические отношения между словами, указания на коммуникативный тип предложения, его эмоциональную окраску, законченность, а также некоторые иные функции.

                                                  С точки зрения непрограммиста и нематематика: «1,2,3,4,5» — это числа разделенные запятми. Обратите внимание, что каким именно образом получилась эта строка (например, в результате объединения коллеции чисел) неважно. Результат один и то же — это CSV.
                                                    0
                                                    С точки зрения непрограммиста типов данных не существует, и число 123 и строка «123» — это одно и то же. А в программировании это не так.
                                                    –1
                                                    По поводу пустого рзделителя. Я понимаю, что фраза: "«7956728893045501» — это числа, разделенные пустой строкой" звучит странно, но тем не мение это правда (допустим, это номер кредитной карты).
                                              –1
                                              Вот хоть убейте, Separate должен возвращать нечто разделенное

                                              Если вызывать его на чём-то целом, да. Но когда у вас уже есть нечто разделённое, Separated(with: ", "), как мне кажется, вполне однозначно говорит, что произойдёт. Не ожидаю же я, что мы разделим уже разделённое?


                                              Кстати, именованный параметр with обязателен. Сделать его таковым язык не позволяет, но без него конструкция неполна.

                                                0
                                                Если вызывать его на чём-то целом, да. Но когда у вас уже есть нечто разделённое, Separated(with: ", ")...

                                                … как мне кажется, вызывает ступор и InvalidOperationException в мозгу.

                                                SQL шел (идет?) по пути приближения к человеческому языку. Результат, как минимум, спорный, но и там не стали для объединения строк использовать ключевое слово SEPARATED BY. Назвать функцию STRING_AGG, думаю, тоже не было идеальным решением, но она хотя бы образована от AGGREGATE, не SEPARATE.
                                                  –1
                                                  … как мне кажется, вызывает ступор и InvalidOperationException в мозгу.

                                                  Открываю руководство по расстановке запятых в английском языке, вижу "Thus, happy and lively are coordinate adjectives in the example and should be separated by a comma.".


                                                  Вот ещё по теме.


                                                  На мой взгляд, тут нужен mind-shift с императивного мышления на декларативное, где нет инструкций, а есть описания.

                                              0
                                              Разумеется, о том, какие предложения языка считать более понятными, а какие менее — можно отдельно спорить. Но хорошо бы хотя бы начать вести такие споры.

                                              Тут я согласен. Поэтому поспорю :)


                                              Я не уверен, что проблема решается без создания нового языка программирования (который, естественно, имеет все шансы не взлететь, а также все шансы превратить проблему в "теперь у нас n+1 язык, который разработчику в наши времена всяко нужно знать"). С точки зрения естественного языка, например, одна из проблем — это то, что действия могут быть без субъекта и с субъектом. И первые логично бы записывать в префиксной нотации и называть в повелительном наклонении, а вторые — в постфиксной. "Чисто объектные" языки не позволяют одно, "чисто функциональные" — другое, а на смешанных почему-то оказывается написано очень много спагетти-кода. Также есть легаси — программисты банально привыкли, что вызов метода идёт через точку от объекта, хотя это, в общем-то, и необязательно.


                                              Другое решение — разрешить программисту лепить ad hoc конструкции и делать свои DSL для отдельных частей кодовой базы. Но тут из примеров только Лиспы, где с помощью макросов можно "исправить" практически любую "неудачную" конструкцию. Нужно, например, много работать с массивами, а (aref array index) писать всё время долго — не проблема. Оборачиваем код в макрос и можем вместо этого писать просто (array index). Но из не-лиспов я такого сорта макросы видел только в Julia, и там "человеческая" запись выражений только мешает их писать.

                                                0
                                                Я не уверен, что проблема решается без создания нового языка программирования (который, естественно, имеет все шансы не взлететь, а также все шансы превратить проблему в "теперь у нас n+1 язык, который разработчику в наши времена всяко нужно знать"). С точки зрения естественного языка, например, одна из проблем — это то, что действия могут быть без субъекта и с субъектом. И первые логично бы записывать в префиксной нотации и называть в повелительном наклонении, а вторые — в постфиксной. "Чисто объектные" языки не позволяют одно, "чисто функциональные" — другое, а на смешанных почему-то оказывается написано очень много спагетти-кода. Также есть легаси — программисты банально привыкли, что вызов метода идёт через точку от объекта, хотя это, в общем-то, и необязательно.

                                                Как мне кажется, C# в этом смысле пока сильнее многих других языков: он позволяет создавать и субъектные выражения с помощью методов расширений, и безсубъектные с помощью using static. Кроме этого, он разрешает именованные параметры, а это также многократно улучшает читаемость и fluent-составляющую многих вызовов. Но мы всё ещё на этапе хелперов.


                                                Другое решение — разрешить программисту лепить ad hoc конструкции и делать свои DSL для отдельных частей кодовой базы. Но тут из примеров только Лиспы, где с помощью макросов можно "исправить" практически любую "неудачную" конструкцию. Нужно, например, много работать с массивами, а (aref array index) писать всё время долго — не проблема. Оборачиваем код в макрос и можем вместо этого писать просто (array index). Но из не-лиспов я такого сорта макросы видел только в Julia, и там "человеческая" запись выражений только мешает их писать.

                                                Как раз недавно искал какие-то новые языки, которые переосмысливают всю ту императивную кашу, которая осталась ещё с доисторических времён. Понравился Nim. Там кроме элегантного синтаксиса, есть шаблоны и макросы, позволяющие настроить язык под себя, как вы и сказали.

                                                  +1
                                                  Fluent-подход отлично заходит, когда видишь одну строчку кода, и по ней понятно, что тут происходит. Например, создание маппинга в автомаппере:

                                                  configuration.CreateMap<OneThing, OtherThing>()
                                                  .ForMember(d => d.StatusId, ctx => ctx.MapFrom(s => s.Status))

                                                  (у меня тут правда большой вопрос к авторам, почему в CreateMap сначала Source, потом Destination, а в маппинге полей — наоборот. Ну да ладно, это к делу не относится)

                                                  Но когда в маппинг надо добавить выражения для 5, 10, 20 полей эта многословность начинает бесить. Куда выразительнее написать, к примеру,

                                                  configuration.CreateMap<OneThing, OtherThing>()
                                                  {
                                                  Status => StatusId,
                                                  Price => Cost,
                                                  Amount => Quantity,
                                                  Count => Number
                                                  };


                                                  Конечно, учитывая ограничения и синтаксис языка, это писалось бы, скорее так:
                                                  { s => s.Price, d => d.Cost },. И тут, не зная контекста, сложнее понять что происходит, чем в случае fleunt-подхода. Но при многогратном повторении смотрится чище.
                                                    +2

                                                    Судя по комментариям дальше, во мнении "коллегам удобно думать по-другому" я всё же не одинок.


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


                                                    var name1 = some_file_name;
                                                    var name2 = name1.Without(".exe");
                                                    var x = name2.AtEnd;

                                                    Ожидание — получить то, что на конце у name2, а name2 — это name1 без ".exe". x — это последняя буква имени файла без расширения! Разве не так? Именно это же написано.


                                                    Это не говоря о контекстной зависимости естественного языка, т.к. я обычно не добавляю "на конце" к фразе "имя файла без .exe". Ваши конструкции вынуждают всегда уточнять, где именно искать подстроку, т.е. программа всё равно пишется на некотором формальном языке, а не на естественном. А раз так, то мне лично было бы уже всё равно — это будет s.Without(".exe").AtEnd, s.RemoveSubstringFromEnd(".exe") или StringTools.RemoveSubstringFromEnd(".exe", s). Но если .AtEnd ещё терпимо, то вот необходимость писать s.Without(substr).Anywhere как-то совсем на естественный язык не похоже.


                                                    Как мне кажется, вам всё-таки надо брать язык с развитым метапрограммированием и демонстрировать применение этого подхода там, потому что там возможности расширения синтаксиса есть by design, без натягивания совы на глобус.

                                                      0
                                                      В итоге, делаются промежуточные объекты, бесполезные сами по себе.

                                                      А как быть, например, с FluentAssertions, которые позволяют:


                                                      // И так.
                                                      instance.Should().BeNotNull();
                                                      // И этак:
                                                      instance.Should().Should().Should().Should();
                                                      // И даже так:
                                                      var x = instance.Should();
                                                      var y = x.BeNull();

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


                                                      А раз так, то мне лично было бы уже всё равно — это будет s.Without(".exe").AtEnd, s.RemoveSubstringFromEnd(".exe") или StringTools.RemoveSubstringFromEnd(".exe", s)

                                                      Интересно, как из того факта, что "писать приходится на формальном языке" вырастает StringTools? Почему его не нужно всегда уточнять, а AtEnd нужно? Как отменяется сама форма естественного языка: субъект предложения, объект? Почему из невозможности написать совсем понятно, следует то, что нужно писать совсем непонятно?


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


                                                      Да, среди множества способов улучшения читаемости, существуют такие, которые поддерживаются языком или недостаточно полно, или совсем частично. Но из этого, как мне кажется, не следует, что:
                                                      а) так писать не нужно;
                                                      b) нужен другой язык;
                                                      c) нужно писать Tools и Utils.

                                                        +1

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


                                                        Когда вы знаете грамматику, вам и в голову не придёт коверкать её только потому, что это возможно.

                                                        А где коверкание в x.Without(".exe")? Я вижу грамматически корректное выражение.


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

                                                        КакМнеКажется.Эта(Идея).ПлохоПодходит(для: РеализацииВ(C#)).


                                                        Просто с метапрограммированием и макросами вы можете реально ввести новый синтаксис, выглядящий как предложения. И это как раз будет нормально демонстрировать идею. А это вот We.Can(Write.CallChainsThatLook(like: "real sentences")) — по выражению Кристиана Шафмайстера, по сравнению с макросами Лиспа — что налоговая декларация по сравнению с сонетами Шекспира.

                                                          0
                                                          А где коверкание в x.Without(".exe")? Я вижу грамматически корректное выражение.

                                                          В x.Without(".exe") его нет. Оно есть в x.Without(".exe").AtEnd. Вот какое:


                                                          var name1 = some_file_name;
                                                          var name2 = name1.Without(".exe");
                                                          var x = name2.AtEnd;

                                                          КакМнеКажется.Эта(Идея).ПлохоПодходит(для: РеализацииВ(C#)).

                                                          Если дискуссия об этом, то не скажу, что совсем уж плохо, но что есть ряд особенностей — это верно.


                                                          И это как раз будет нормально демонстрировать идею.

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


                                                          Посмотрите, например, на Spark. Вот пример API сохранения сразу на нескольких языках:


                                                          Python


                                                          x.write.save("...")
                                                          x.write.format("json").save("...")

                                                          Java


                                                          x.write().save("...")
                                                          x.write().format("json").save("...")

                                                          Не будем же мы писать вот такое:


                                                          a = x.write
                                                          b = a.format("json")
                                                          c = b.save("...")

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


                                                          А это вот We.Can(Write.CallChainsThatLook(like: "real sentences")) — по выражению Кристиана Шафмайстера, по сравнению с макросами Лиспа — что налоговая декларация по сравнению с сонетами Шекспира.

                                                          Сравнение броское, но, как это часто бывает со сравнениями, отвлекает от сути. Чья-то действительность — C# или Java. В которых можно писать красивее, элегантнее, проще и опрятнее, чем GetUserById или UtilsManager.

                                                            +1
                                                            Но идея как раз в том, чтобы писать код понятно, отвлечённо от языка. Если вы можете, то почему нет?

                                                            Так весь сыр-бор к тому и есть — одному "наиболее понятным и естественным" одно, другому другое. А то, что язык искусственно созданный и формализованный — не значит, что там нет каких-то своих традиций и идиом. Тащить туда конструкции естественного языка "чтобы было удобнее читать" — это делать какую-то смесь французского с нижегородским.


                                                            Не будем же мы писать вот такое:
                                                            a = x.write
                                                            b = a.format("json")
                                                            c = b.save("...")


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

                                                            Так тут как раз всё вроде понятно, кроме того, что x.write не записывает непосредственно. И, опять же, интерфейс не стремится быть похожим на естественный язык ради похожести на естественный язык.


                                                            В целом, то, что надо стремиться, учиться и практиковаться писать кратко, красиво и понятно — это бесспорно. Но с какого-то момента начинается притягивание за уши. В языке нельзя вставлять аргумент посередине имени оператора — придумаем .Without("t").AtEnd. Ну чем оно принципиально лучше s.RemoveSubstring("suffix", fromEnd: true, count: 1) или хотя бы s.Without("t", at: "end")? Во втором случае s.Without("substring") вполне может возвращать строку без подстроки без необходимости городить дополнительные .Anywhere, бонусом можно этот метод реализовать и для других коллекций, а не только строк.

                                                              0
                                                              В языке нельзя вставлять аргумент посередине имени оператора — придумаем .Without("t").AtEnd.

                                                              Но почему нет?


                                                              Ну чем оно принципиально лучше s.RemoveSubstring("suffix", fromEnd: true, count: 1)

                                                              Давайте отойдём на секунду от x.Without(y).AtEnd, и посмотрим только на предложенное s.RemoveSubstring("suffix", fromEnd: true, count: 1) с точки зрения понятности и лаконичности.


                                                              1. Remove хуже, потому что имеется ввиду не Remove, а WithRemoved. Нужно что-то, что покажет, что результирующая строка — это исходная без какой-то подстроки.
                                                              2. Substring — как мне кажется, лишнее. Удаляя что-то из строки, мы итак понимаем, что это что-то — подстрока (substring).
                                                              3. count: 1 — что-то новенькое. Опустим, как поведение по умолчанию.

                                                              Пока получается s.Without("suffix", fromEnd: true). Но что значит тогда: s.Without("suffix", fromEnd: false)? Вообще везде или на старте?


                                                              Можно было бы: s.Without("suffix", From.End) или s.Without("suffix", From.Start), но тогда перечисление From получается слишком общего назначения.


                                                              Хотя вот ещё: s.Without("suffix", from: End), где End — значение перечисления с более конкретным именем, например, PositionKind. Его импортируем через using static, и готово. Но это попросту неудобно, каждый раз писать и импортировать.


                                                              Можно через лямбды: s.Without("suffix", _ => _.FromEnd), s.Without("suffix", _ => _.FromStart). Но это тоже не очень удобно и громоздко.


                                                              s.Without("suffix", from: "end") — упираемся в возможность ошибиться при вводе "end".


                                                              Вероятно, лучше всего использовать совет от qw1 и взять s.WithoutSuffix("Builder"), s.WithoutPrefix("Abstract") и s.Without("part") как замены предложенным мной вариантам с цепочками.


                                                              Да, ещё можно остановиться на s.RemoveFromStart("suffix"). Как можно остановиться на Directory.CreateDirectory или QueueUserWorkItem.


                                                              Но, на мой взгляд, мы как раз делаем то, что нужно: размышляем на тему того, как подобрать однозначное и читаемое название, используя ограничения того языка, который имеется.


                                                              А то, что язык искусственно созданный и формализованный — не значит, что там нет каких-то своих традиций и идиом. Тащить туда конструкции естественного языка "чтобы было удобнее читать" — это делать какую-то смесь французского с нижегородским.

                                                              К счастью, традиции и идиомы устаревают. И уже сегодня написано огромное количество fluent-библиотек (хотя не все они Fluent). Скажем, FluentAssertions и NSubstitute.

                                                                +2
                                                                Вероятно, лучше всего использовать совет от qw1 и взять s.WithoutSuffix("Builder"), s.WithoutPrefix("Abstract") и s.Without("part") как замены предложенным мной вариантам с цепочками.

                                                                С этим согласен, особенно если суффиксы или префиксы из строк приходится убирать часто.


                                                                Ещё я понял, что куда-то со своими рассуждениями ушёл в сторону, потому что существенная часть — я тоже вижу проблему в стиле написания ObjectFactory.CreateObjectFromInput(InputContainer.Input).


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


                                                                Поэтому, например, x.Should().BeEqualTo(42, "because reasons") мне не нравится — assert уже вполне устоявшаяся идиома в программировании, причём в обычной речи слово редко встречается, а "should" выглядит как-то бесформенно.


                                                                То же самое относительно примера с Max() — это математическая нотация, так же как и символ присваивания в том же самом выражении. C Number.ButNotLessThan(LowerBound) теряется тонкая математическая красота и равнозначность аргументов, вместо неё одно значение становится "ведущим", т.к. метод вызывается из него (ну да, я технарь, я так вижу). Относительно близкая к естественному языку конструкция, которая у меня ещё не вызывает такого отторжения — что-то вроде GreaterOf(2, evils). Насчёт a > b ? a : b согласен — хотя и читаемо в принципе, но не очень красиво. В частности, потому, что Max() сразу говорит, что сравниваются числа, а в тернарном операторе это ещё визуально распарсить надо.

                                                                  0

                                                                  Насчёт Max тоже согласен с тем, что он, вероятно, лучше всего подходит для большинства случаев, особенно в записи вида: Max(of: a, b).


                                                                  Но иногда получается вот как: скажем, если минимум здоровья — 0, то мы пишем при этом Health = Max(of: Health - Damage, 0), что, на мой взгляд, именно в этом конкретном случае не так интуитивно, как хотелось бы. Поэтому Health = (Health - Damage).ButNotLess(than: 0) как будто лучше передаёт идею.

                                                                    +1

                                                                    Конкретно для этого случая — скорее, нужно иметь метод Character.TakeDamage(points), который всем этим занимается.


                                                                    Вот, кстати, возникает вопрос тоже. В вашем стиле получается, что нужно писать hero.Take(damage) вместо hero.TakeDamage(damage). Но как тогда отличать Take(damage) от Take(pen)? Т.е. понятно, что можно сказать: Take — это у нас именно для принятия урона, а для "взятия" предмета у нас Pick, а для "выбора" (pick — это ж ещё "выбирать") будет ChooseOneAmong(). Но это же улучшит читаемость только правильно написанного кода, зато усложнит написание и кодревью — hero.Take(pen) vs hero.Pick(pen) не выглядит очевидной ошибкой, в отличие от hero.TakeDamage(pen) vs hero.PickObject(pen).


                                                                    Ещё мне не очень нравится, что статические по сути методы вы предлагаете делать методами объекта. Во-первых, теряется единообразие — например, наряду с Math.sqrt(x) может возникнуть x.WithoutSign() вместо Math.Abs(x) "потому что мы говорим 'корень из x' и 'x без знака'" (а "модуль" — для наших разработчиков слишком сложное понятие). Во-вторых, нарушается пресловутая идиоматика "операции без побочных эффектов — в статические методы, изменение состояния — в методы объекта". В-третьих, как-то странно, что объект должен знать, какие функции можно вычислить от его значения — попахивает нарушением SRP. Далее мои домыслы: если в рантайме методы объекта создаются как лексические замыкания, то при создании каждого нового числа (напр., как результата промежуточного математического выражения) нужно для него создать ещё и все эти замыкания, которые ещё не факт что будут вызваны. Это просто гигантский оверхед на пустом месте.

                                                                      0
                                                                      Далее мои домыслы
                                                                      Методы расширения это просто синтаксический сахар для вызова статического метода, у которого первый параметр — ссылка на объект. Компилятор даже позволяет использовать оба синтаксиса для вызова метода расширения:
                                                                      как
                                                                      player.TakeDamage(500),
                                                                      так и
                                                                      PlayerUtil.TakeDamage(player, 500);

                                                                      В последнем случае можно указывать префиксом полный namespace (и если надо, сборку из которой импортирован метод), и разрешать конфликты с пересечением наименований.
                                                                        0

                                                                        Спасибо, понял.

                                                                        0
                                                                        Конкретно для этого случая — скорее, нужно иметь метод Character.TakeDamage(points), который всем этим занимается.

                                                                        Но само выражение Health = (Health - damage).ButNotLess(than: 0) где-то написать придётся. Кстати, то же самое касается, например, восстановления здоровья. На мой взгляд, запись вида: Health = Min(of: Health + heal, MaxHealth) заставляет задуматься, а вот: Health = (Health + heal).ButNotGreater(than: MaxHealth) — совсем fluent.


                                                                        В вашем стиле получается, что нужно писать hero.Take(damage) вместо hero.TakeDamage(damage). Но как тогда отличать Take(damage) от Take(pen)?

                                                                        Альтернатив много:
                                                                        character.Take(damage) и character.Inventory.Put(item),
                                                                        character.Take(damage) и character.Pickup(item) и т.д.


                                                                        Но в играх предметная область слишком сложна, чтобы можно было ограничиться такими простыми выражениями. Скорее всего, здоровье придётся менять напрямую с помощью character.Health.Change(by: -damage), ибо есть промах, крит. удар, броня, эффекты, и т.д.


                                                                        Если совсем не избежать, то дублирование по имени параметра hero.TakeDamage(damage) не та уж страшно (хотя подумать, как избежать, точно стоит), как, скажем, Directory.CreateDirectory (но почему-то Directory.Delete). Они качественно отличаются.


                                                                        Во-первых, теряется единообразие

                                                                        Единообразие теряется только в каких-то отдельных случаях, но и этим можно пожертвовать. Общая идея методов расширений, как я уже писал, не просто в синтаксисе, а и в семантике того, что получается в записи: у статического метода появляется субъект. Например: char.IsDigit(c) не то же самое, что c.IsDigit().


                                                                        Во-вторых, нарушается пресловутая идиоматика "операции без побочных эффектов — в статические методы, изменение состояния — в методы объекта"

                                                                        Впервые слышу об этой идиоматике. Что она постулирует и чем чревато её нарушение?

                                                  0
                                                  В целом, подход JoshuaLight хорош, но чтобы понять, как разрабатывают API коллеги из Microsoft, рекомендую книгу Цвалина К., Абрамс Б. «Инфраструктура программных проектов: соглашения, идиомы и шаблоны для многократно используемых библиотек .NET». Там подробно написано, почему API .NET именно такое, чем они довольны и в чем раскаиваются.
                                                    0
                                                    Часть этих штук можно посмотреть у меня на GitHub.
                                                    Ну так, полистал и не нашёл всякое типа
                                                    .Without(".exe").AtEnd;
                                                    .Or(0).IfGreater();
                                                    .ButNotLess(than: 0);

                                                    Лучше бы написали, как такое реализовывать и не запутаться в простынях кода.
                                                    Пользоваться удобно, но

                                                    1) писать такое тяжко, т.е. в одноразовых (не core) классах, которые переписываются часто, не применимо.

                                                    2) если писать такое в core-классах, возникает overhead на создание промежуточных классов и копирования в них всего констекста.

                                                    А между прочим, какое-то время назад тут было интервью с экспертом по перфомансу в .NET, который советовал не париться насчёт производительности бизнес-логики, которую всё равно пишут люди разной квалификации, но вот core, common, util и т.п. серьёзно оптимизировать (вплоть до отказа от linq), потому что эти методы в проекте вызываются постоянно и однажды плохо написанные, будут всплывать в профайлере постоянно.
                                                      +1
                                                      Ну так, полистал и не нашёл всякое типа

                                                      А вот же.


                                                      Лучше бы написали, как такое реализовывать и не запутаться в простынях кода.

                                                      Это размазало бы статью. Особенно если учесть, что реализовать такое нехитро, речь ведь о форме и именах, а не алгоритмах.


                                                      писать такое тяжко

                                                      Не уверен. Как правило, так же, как и всё остальное. Что непривычно кому-то, кто всю жизнь писал GetUserById, — это верно.


                                                      если писать такое в core-классах, возникает overhead на создание промежуточных классов

                                                      Если вы про создание в рантайме, то overhead'а почти всегда нет, ведь создаются структуры. Если про написание кода, то да, определённый overhead появляется ввиду того, что в языке C# нет удобных способов реализовывать однозначные цепочки запросов.


                                                      А между прочим, какое-то время назад тут было интервью с экспертом по перфомансу в .NET, который советовал не париться насчёт производительности бизнес-логики, которую всё равно пишут люди разной квалификации, но вот core, common, util и т.п. серьёзно оптимизировать (вплоть до отказа от linq), потому что эти методы в проекте вызываются постоянно и однажды плохо написанные, будут всплывать в профайлере постоянно.

                                                      Большая часть материала — про имена и названия (которые определяют содержание), так что на производительность они никак не влияют.


                                                      В вопросах производительности контекст первичен. Иначе разработчики платформы .NET не написали бы LINQ, следуя указанным выше критериям.


                                                      Иными словами, если красивый код чего-то стоит, пусть стоит, не вызывайте его в while (true).


                                                      В моём опыте в основном было так: люди считают замыкания и упаковки, не понимая количество вызовов и общий трафик; избавляются от LINQ (пусть и в Common), хотя общая тенденция проектов такая, что 99% кода не влияют на производительность. Всё это излишне. Как правило, и так понятно: "О, вот тут что-то мне надо рекурсивно будет считать, теоретический масштаб вот какой, тогда, пожалуй, проверю, как оно себя ведёт, и подумаю, заменить ли List на HashSet".

                                                        +1
                                                        Все аргументы справедливы.

                                                        Но лично для меня, конструкция
                                                        .Without(".exe").AtEnd;
                                                        имеет большие затраты на чтение и работу с ней, чем обычное
                                                        Path.GetFileNameWithoutExtension(...)

                                                        Я всегда стараюсь понимать до конца, что происходит в каждой строке. Мне не нравится подход «пиши английский текст, и не забивай себе голову тем, что под капотом, библиотека сама тебя поймёт и сделает правильно».

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

                                                        Во втором случае — обычный вызов статического метода, который делает ровно то, что у него в названии. Никаких лишних сущностей.
                                                          0
                                                          Мне не нравится подход «пиши английский текст, и не забивай себе голову тем, что под капотом, библиотека сама тебя поймёт и сделает правильно».

                                                          Не уверен, что утверждал именно это, предлагая x.Without(y).AtEnd.


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

                                                          Если смотреть на это с такой точки зрения, то да. Поэтому я и предлагаю совершить ментальный скачок (в статьях описывая его суть): смотреть на код не только как на объекты, методы, вызовы, свойства, а как на выражения и предложения.


                                                          С такой точки зрения "Name_postfix".Without("_postfix").AtEnd читается ровно так, как вы бы ожидали услышать в естественной речи. Или, скажем:


                                                          // На мой взгляд, вполне очевидно, что будет в переменной `x`.
                                                          var x = "D:\\git\\Repository".AllAfter("git\\");

                                                          Это может показаться магией, но по тем же причинам магией может показаться управление памятью или даже сама платформа .NET. И тем не менее, мы обобщаем штуки, чтобы писать более красивые и удобные штуки, чтобы потом, обобщая их, писать кое-что ещё лучше.

                                                            0
                                                            Почему эти идеи встречают неприятие? Они заставляют переучиваться на новый стиль, и неизбежно ухудшать навыки понимания стиля, уже принятого повсеместно. Перейдя на новый стиль, старый код будет раздражать и появится желание всё переписать. Но в то же время, объективных плюсов (как, например, при переходе с C++ на C#) я не вижу, просто другой синтаксис.

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

                                                            Другое дело, если бы была перспектива — выучу немецкий и код буду писать в 3 раза быстрее. Но нет же — те же яйца в другой профиль. А ещё все вокруг пишут на английском, но вам нужно ваш код писать на немецком.
                                                              0
                                                              Они заставляют переучиваться на новый стиль

                                                              Писать понятный и читаемый код — это не новый стиль.


                                                              Ещё лет двадцать назад Гради Буч в книге "Object Oriented Analysis and Design with Applications" цитировал другую работу 1989 года (Lins, C. 1989. A First Look at Literate Programming. Structured Programming.) — "Software should be written as carefully as English prose, with consideration given to the reader as well as to the computer".


                                                              Ему же приписывают: "Clean code reads like well-written prose".


                                                              Другое дело, если бы была перспектива — выучу немецкий и код буду писать в 3 раза быстрее. Но нет же — те же яйца в другой профиль. А ещё все вокруг пишут на английском, но вам нужно ваш код писать на немецком.

                                                              На мой взгляд, разница утрирована. Куда точнее: "Все вокруг пишут философские трактаты, а нужно, оказывается, писать понятно и просто? Зачем?".

                                                                +2
                                                                Это не просто и понятно, это какой-то ребус…
                                                                Вот реально,
                                                                fileName.Without(".exe").AtEnd
                                                                для меня читается как
                                                                Скрытый текст

                                                                Скрытый текст


                                                                Намного лучше же глагол+существительное:
                                                                fileName.RemoveSuffix(".exe")
                                                                  0
                                                                  Намного лучше же глагол+существительное:

                                                                  Непонятен ваш критерий. Мой критерий — английский язык. Неужели, если в коде написано fileName.Without(".exe").AtEnd — ребус, а вот если скажет заказчик: "Here I'll need to show file name without .exe at end, can you do that?", то вопросов нет?


                                                                  Кстати, если "show file name without extension", то сразу: fileName.WithoutExtension() (что предполагает AtEnd).


                                                                  fileName.RemoveSuffix(".exe")

                                                                  Но fileName не меняется после вызова Remove. Кроме того, суффикс — это понятие слова, а не целой строки.

                                                                    +1
                                                                    Непонятен ваш критерий. Мой критерий — английский язык
                                                                    Критерий — минимальная сложность. Конструкция fileName.Without логически незакончена, к ней нужна одна функция для завершения.
                                                                    Но fileName не меняется после вызова Remove
                                                                    Получается название в функциональном стиле, где результат это применение функции ко входу, а вход не меняется )))

                                                                    Кроме того, суффикс — это понятие слова, а не целой строки.
                                                                    В computer science, суффикс это любая подстрока, начинающаяся с некоторой позиции до конца строки (см. суффиксное дерево, например).
                                                                      –2
                                                                      Критерий — минимальная сложность. Конструкция fileName.Without логически незакончена, к ней нужна одна функция для завершения.

                                                                      Из этого не следует, что она сложнее. Для LINQ запросов тоже нужны функции для завершения, и, тем не менее, они не являются сложными.

                                                                        +1
                                                                        В LINQ-запросах промежуточные результаты являются осмысленными, и не требуют специальных «конструкций для завершения».
                                                                        0
                                                                        В computer science, суффикс это любая подстрока, начинающаяся с некоторой позиции до конца строки (см. суффиксное дерево, например).

                                                                        Кстати, верно, этого я не знал, прошу прощения!


                                                                        Тем не менее, сравните сложность слов Remove и Suffix и Without и End (At мы в расчёт не берём). В первом случае вам нужно привлекать специалиста по computer science, а во втором — не обязательно. При этом мы, разумеется, ещё учитываем почти полную тождественность Without(x).AtEnd тому, как это произносится в речи.


                                                                        Вот то самое стремление к минимальной сложности (т.е. к упрощению) и получается.

                                                                          0
                                                                          Я считаю сложность не по длине идентификаторов (тут auto-complete помогает), а по количеству синтаксических единиц. RemoveSuffix — один токен, Without+AtEnd+лишняя точка — три токена.
                                                                            –1
                                                                            Я считаю сложность не по длине идентификаторов

                                                                            Я тоже.

                                                        +1
                                                        "D:\Source".AsDirectory().Copy().Files.To("D:\Target")

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


                                                        "D:\\Source".AsDirectory().Delete().Files.Where(f => f.Name=="temp.txt")

                                                        Обычно пользователь сначала выбирает файлы, а потом действие.

                                                          0
                                                          Обычно пользователь сначала выбирает файлы, а потом действие.

                                                          Но говорим-то мы "copy files".

                                                            0
                                                            Возращаемся обратно к
                                                            CopyUtil.Copy(files);
                                                            или, используя языковые возможности C# 6,
                                                            using static CopyUtil;
                                                            Copy(files);

                                                            (если не нравится указывать префикс класса)
                                                              –1
                                                              CopyUtil.Copy(files);

                                                              Потерялась папка.


                                                              Я бы предложил тогда:


                                                              Copy.Files(from: source, to: destination);

                                                              Зачем CopyUtil? Что это такое?

                                                                +1
                                                                Стиль мышления технаря против стиля мышления гуманитария.

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

                                                                Класс Copy — это что? Копия? Нет, это класс, выполняющий копирование, т.е. копировщик (Copier, например).

                                                                Метод Files — это что? В отрыве от имени класса вообще бред.

                                                                Т.е. для аналитического типа мышления это категорически не заходит. Читать код как поэму, на одном дыхании — увы, не ко мне.
                                                                  0
                                                                  Стиль мышления технаря против стиля мышления гуманитария.

                                                                  Мне кажется, это придуманное, а не фактическое положение вещей.


                                                                  Что технического в CopyUtil?


                                                                  Нет, это класс, выполняющий копирование

                                                                  Это и есть копирование.


                                                                  В отрыве от имени класса вообще бред.

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


                                                                  Мало написать и реализовать алгоритм — это ничто по сравнению с тем, чтобы написать его доступным для других.

                                                                    +1
                                                                    Это и есть копирование.
                                                                    Сразу видно, вы не математик и в формализмы не умеете. Потому что формально, что CopyUtil, что ваш класс Copy — это класс, который… и далее по тексту.
                                                              +2

                                                              То есть, вы привязываете интерфейс к английскому языку.
                                                              Если, например, на хинди порядок слов в предложении другой, то индусам надо выдать принципиально другой API?

                                                                –1
                                                                То есть, вы привязываете интерфейс к английскому языку.

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

                                                            0

                                                            (Промахнулся веткой.)

                                                              +2

                                                              На мой взгляд, главная проблема с написанием кода, максимально похожего на естественный язык, в том, что фразы на естественном языке многозначны и могут быть истолкованы по разному. Часть работы программиста как раз и заключается в том, чтобы описание задачи на естественном языке перевести в однозначный формальный язык и от этого "перевода" никуда не деться. Взять хотя бы ваш IfLess. Вызывающему коду предоставляются многозначные "фразы" вроде "a или b, если меньше", а внутри — строго однозначный тернарный оператор.


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

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

                                                              Самое читаемое