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


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


Эпилог


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


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


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


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


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


Ссылки


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

Комментарии (88)


  1. tuxi
    12.04.2019 11:28

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

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


    1. JoshuaLight Автор
      12.04.2019 11:49

      можно сократить до GetOrDefault

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


      1. tuxi
        12.04.2019 12:04

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


        1. JoshuaLight Автор
          12.04.2019 12:05

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


          1. tuxi
            12.04.2019 12:08

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


            1. JoshuaLight Автор
              12.04.2019 12:30

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


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


              1. tuxi
                12.04.2019 12:35

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


                1. JoshuaLight Автор
                  12.04.2019 12:55

                  Я и уточнил, что подразумевается под «это не действие»

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


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


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


                  1. Simplifier
                    13.04.2019 00:25

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


                    1. JoshuaLight Автор
                      13.04.2019 00:28

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


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


          1. IvanNochnoy
            12.04.2019 12:45

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


            1. JoshuaLight Автор
              12.04.2019 12:58

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


              1. IvanNochnoy
                12.04.2019 13:41

                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.


                1. JoshuaLight Автор
                  12.04.2019 14:28

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


                  Because methods are the means of taking action

                  Это неверно.


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


                  • Where, Select и т.д.;
                  • Enumerable.Empty<T>();
                  • ImmutableArray.As<T>().
                  • И т.д.


                  1. IvanNochnoy
                    12.04.2019 15:00

                    Where, Select выполняются мгновенно, так как вычисления LINQ-выражений ленивые;

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

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

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


      1. mkshma
        12.04.2019 13:29

        А потом, следуя советам второй статьи, мы убираем Get

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


  1. aamonster
    12.04.2019 11:47

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

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


    1. JoshuaLight Автор
      12.04.2019 11:52

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


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


  1. IvanNochnoy
    12.04.2019 11:56

    Выражению «или 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. JoshuaLight Автор
      12.04.2019 12:12

      На мой взгляд, вы используете один и тот же критерий для оценки двух совершенно различных подходов к написанию кода. Так можно и про 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 []


      1. IvanNochnoy
        12.04.2019 12:36

        Вы понимете разницу между default(int) и default(int?). Вы понимаете, что x.GetValueOrDefault() и x.OrDefault() это совсем не одно и то же?

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

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


        1. JoshuaLight Автор
          12.04.2019 14:40

          Вы понимете разницу между default(int) и default(int?). Вы понимаете, что x.GetValueOrDefault() и x.OrDefault() это совсем не одно и то же?

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


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


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

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


  1. MonkAlex
    12.04.2019 12:09

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

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

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


    1. JoshuaLight Автор
      12.04.2019 12:14

      заказы всё

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


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

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


      1. MonkAlex
        12.04.2019 12:17

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

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


        1. JoshuaLight Автор
          12.04.2019 12:46

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

          На мой взгляд, напротив: нужно всегда помнить, что у каждого вызова есть контекст, например, методы вызываются или в контексте имени переменной, или в контексте имени класса (если метод статический). Иначе получится 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. alex1t
    12.04.2019 12:19

    Теперь не хватает только реализации всего этого.


    1. JoshuaLight Автор
      12.04.2019 12:50

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


  1. IvanNochnoy
    12.04.2019 12:27

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

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


    1. JoshuaLight Автор
      12.04.2019 12:48

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

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


      1. IvanNochnoy
        12.04.2019 13:20

        Упс, а должен был бы.


  1. IvanNochnoy
    12.04.2019 13:03

    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", "")?
    


    1. JoshuaLight Автор
      12.04.2019 16:23
      -1

      // Реальный классический подход
      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. IvanNochnoy
    12.04.2019 13:31

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

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


    1. JoshuaLight Автор
      12.04.2019 16:38

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


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


      Creature creature = ...;
      int damage = ...;
      
      // Было.
      creature.Health = (creature.Health - damage).Or(0).IfLess();
      
      // Стало.
      creature.Health = (creature.Health - damage).ButNotLess(than: 0);


      1. IvanNochnoy
        12.04.2019 17:40

        За инвариантом Health >= 0 должен следить класс Creature, это его зона ответственности

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


        1. JoshuaLight Автор
          12.04.2019 17:45

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


      1. pankraty
        13.04.2019 08:21

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


        1. JoshuaLight Автор
          13.04.2019 10:36

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


  1. eefadeev
    12.04.2019 13:34

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


  1. IvanNochnoy
    12.04.2019 14:02

    Чувствую себя сукой, но я просто вначле читаю Ваш код, а уже затем комментарии к нему, чесслово.

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

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


    1. JoshuaLight Автор
      12.04.2019 15:02

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

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


      1. IvanNochnoy
        12.04.2019 15:22

        В моем понимании «HelloWorld».PascalCaseWords().Separated(with: " "); вернет «HelloWorld», а у Вас он должен вернуть «Hello World».


      1. Quilin
        13.04.2019 12:52

        Во всем этом семантичном и текстовом коде есть главная проблема. Программисты все сплошь в лучшем случае б2 (конечно, за исключениями), и поскольку кроме как на галеры английский язык на собесах не спрашивают (да и там не парятся), получаются вот эти перлы типа separated with вместо by.
        Вот мы например по этой причине отказались от английского геркина в bdd и перешли на русский.


        1. JoshuaLight Автор
          13.04.2019 14:17

          Программисты все сплошь в лучшем случае б2

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


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

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


  1. IvanNochnoy
    12.04.2019 14:36

    // `using static` всё выровнял! Читается прекрасно.
    var x = items.All(Weapons);
    

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


    1. JoshuaLight Автор
      12.04.2019 15:07
      -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. Pand5461
    12.04.2019 19:03

    Тема Лисп-макросов не раскрыта :)
    Не, к 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 ", ")
    


  1. JoshuaLight Автор
    12.04.2019 19:21

    Главное достоинство — программист пишет так, как ему удобно думать. Главный недостаток — его коллегам удобно думать по-другому.

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


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


    1. pankraty
      12.04.2019 20:27

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


      1. IvanNochnoy
        12.04.2019 22:08

        Кстати, вот если язык брать как критерий првильности, то как правильно говорить: список, разделенный запятыми, или список, объединенный запятыми?


        1. pankraty
          12.04.2019 22:14

          Когда на входе — коллекция, а на выходе — строка, то это-таки «элементы, соединенные запятыми». Если же наоборот — то можно получить, допустим, список слов, разделенных [в исходной строке] пробелами.


          1. IvanNochnoy
            12.04.2019 23:49

            Так, ясно, Join — это глагол, Separated — это прилагательное, первое — это действие, второе — это результат. Оба варианта правильны (включая вырожденные случаи с пустым разделителем). Имеет ли смысл заменять императивную семантику на декларативную? Я без понятия. Учитывая, что весь .NET написан в императивном стиле, я бы публичные сборки делал так же, а для внутренних нужд можно и декларативно. Главное, чтобы было понятно и однозначно.


            1. pankraty
              13.04.2019 07:39

              Join — это глагол, Separated — это прилагательное, первое — это действие, второе — это результат.

              Даже если оба слова сделать активными (join, separate) или пассивными глаголами (joined, separated), суть не изменяется. Первый про объединение, второй про разделение, и действия эти направлены противоположно. Увидеть separated там, где ожидаешь увидеть joined, не ложится в прицнип наименьшего удивления.


              1. IvanNochnoy
                13.04.2019 11:27

                Зна?ки препина?ния — элементы письменности, выполняющие вспомогательные функции разделения (выделения) смысловых отрезков текста, предложений, словосочетаний, слов, частей слова, указания на грамматические и логические отношения между словами, указания на коммуникативный тип предложения, его эмоциональную окраску, законченность, а также некоторые иные функции.

                С точки зрения непрограммиста и нематематика: «1,2,3,4,5» — это числа разделенные запятми. Обратите внимание, что каким именно образом получилась эта строка (например, в результате объединения коллеции чисел) неважно. Результат один и то же — это CSV.


                1. mayorovp
                  15.04.2019 14:41

                  С точки зрения непрограммиста типов данных не существует, и число 123 и строка «123» — это одно и то же. А в программировании это не так.


              1. IvanNochnoy
                13.04.2019 12:34

                По поводу пустого рзделителя. Я понимаю, что фраза: "«7956728893045501» — это числа, разделенные пустой строкой" звучит странно, но тем не мение это правда (допустим, это номер кредитной карты).


      1. JoshuaLight Автор
        13.04.2019 00:12

        Вот хоть убейте, Separate должен возвращать нечто разделенное

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


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


        1. pankraty
          13.04.2019 07:49

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

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

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


          1. JoshuaLight Автор
            13.04.2019 10:47

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

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


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


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


    1. Pand5461
      12.04.2019 22:36

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

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


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


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


      1. JoshuaLight Автор
        13.04.2019 00:23

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

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


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

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


        1. pankraty
          13.04.2019 08:13

          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-подхода. Но при многогратном повторении смотрится чище.


        1. Pand5461
          16.04.2019 08:53
          +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, без натягивания совы на глобус.


  1. IvanNochnoy
    12.04.2019 22:16

    В целом, подход JoshuaLight хорош, но чтобы понять, как разрабатывают API коллеги из Microsoft, рекомендую книгу Цвалина К., Абрамс Б. «Инфраструктура программных проектов: соглашения, идиомы и шаблоны для многократно используемых библиотек .NET». Там подробно написано, почему API .NET именно такое, чем они довольны и в чем раскаиваются.


  1. qw1
    13.04.2019 15:02

    Часть этих штук можно посмотреть у меня на GitHub.
    Ну так, полистал и не нашёл всякое типа
    .Without(".exe").AtEnd;
    .Or(0).IfGreater();
    .ButNotLess(than: 0);

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

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

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

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


    1. JoshuaLight Автор
      13.04.2019 16:26

      Ну так, полистал и не нашёл всякое типа

      А вот же.


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

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


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

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


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

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


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

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


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


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


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


      1. qw1
        14.04.2019 01:37

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

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

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

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

        Во втором случае — обычный вызов статического метода, который делает ровно то, что у него в названии. Никаких лишних сущностей.


        1. JoshuaLight Автор
          15.04.2019 11:35

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

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


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

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


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


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

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


          1. qw1
            15.04.2019 11:56

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

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

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


            1. JoshuaLight Автор
              15.04.2019 13:15

              Они заставляют переучиваться на новый стиль

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


              Ещё лет двадцать назад Гради Буч в книге "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 раза быстрее. Но нет же — те же яйца в другой профиль. А ещё все вокруг пишут на английском, но вам нужно ваш код писать на немецком.

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


              1. qw1
                15.04.2019 13:56
                +1

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

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


                1. JoshuaLight Автор
                  15.04.2019 14:24

                  Намного лучше же глагол+существительное:

                  Непонятен ваш критерий. Мой критерий — английский язык. Неужели, если в коде написано 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. qw1
                    15.04.2019 14:35
                    +1

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

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


                    1. JoshuaLight Автор
                      15.04.2019 16:20
                      -1

                      Критерий — минимальная сложность. Конструкция fileName.Without логически незакончена, к ней нужна одна функция для завершения.

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


                      1. mayorovp
                        15.04.2019 17:11
                        +1

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


                    1. JoshuaLight Автор
                      15.04.2019 16:56

                      В computer science, суффикс это любая подстрока, начинающаяся с некоторой позиции до конца строки (см. суффиксное дерево, например).

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


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


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


                      1. qw1
                        15.04.2019 17:34

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


                        1. JoshuaLight Автор
                          15.04.2019 17:50

                          Я считаю сложность не по длине идентификаторов

                          Я тоже.


  1. Deosis
    15.04.2019 08:08
    +1

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

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


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

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


    1. JoshuaLight Автор
      15.04.2019 11:08

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

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


      1. qw1
        15.04.2019 11:40

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

        (если не нравится указывать префикс класса)


        1. JoshuaLight Автор
          15.04.2019 11:43

          CopyUtil.Copy(files);

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


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


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

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


          1. qw1
            15.04.2019 12:05

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

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

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

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

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


            1. JoshuaLight Автор
              15.04.2019 12:56

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

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


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


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

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


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

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


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


              1. qw1
                15.04.2019 13:58
                +1

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


      1. Deosis
        16.04.2019 09:01
        +2

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


  1. JoshuaLight Автор
    15.04.2019 11:42

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