Для сопровождения программы код приходится читать, и тем это делать проще, чем больше он похож на естественный язык, — тогда быстрее вникаешь и сосредотачиваешься на главном.
В прошлых двух статьях я показал, что тщательно выбранные слова помогают лучше понимать суть написанного, но думать только о них недостаточно, ведь всякое слово существует в двух формах: как само по себе и как часть предложения. Повтор CurrentThread
ещё не повтор, пока мы не читаем его в контексте Thread.CurrentThread
.
Таким образом, ориентируясь в нотах и простых мелодиях, мы посмотрим теперь, что такое музыка.
Оглавление цикла
- Объекты
- Действия и свойства
- Код как текст
Код как текст
Большинство 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)
aamonster
12.04.2019 11:47Для сопровождения программы код приходится читать, и тем это делать проще, чем больше он похож на естественный язык, — тогда быстрее вникаешь и сосредотачиваешься на главном.
Вам бы попробовать AppleScript – возможно, мнение бы изменилось.
Максимум сходства с естественным языком… И это совсем не радует.JoshuaLight Автор
12.04.2019 11:52Максимум сходства с естественным языком обеспечивает разработчик. Язык программирования предоставляет для этого средства: лаконичный синтаксис, методы расширений, UFCS, шаблоны и макросы и т.д.
Что пользоваться этим всем можно не совсем элегантно — тоже верно.
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 Чё за хрень?!
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 []
IvanNochnoy
12.04.2019 12:36Вы понимете разницу между default(int) и default(int?). Вы понимаете, что x.GetValueOrDefault() и x.OrDefault() это совсем не одно и то же?
Ко второму нужно привыкнуть
Нельзя заставлять миллионы разработчиков привыкать, им нужен четкий и понятный API.JoshuaLight Автор
12.04.2019 14:40Вы понимете разницу между default(int) и default(int?). Вы понимаете, что x.GetValueOrDefault() и x.OrDefault() это совсем не одно и то же?
Одно и то же. Вы же не пишете
int? numberOrNull
, а пишетеint? number
(что, по вашей логике, неверно, поскольку там не число, а число или ничего). Некоторые детали отдаются типу и контексту, так что тут, на мой взгляд, всё в порядке.
Более того, мне интересно, как это противоречит самой идее включать название переменной в имя метода и тем самым добиваться fluent.
Нельзя заставлять миллионы разработчиков привыкать, им нужен четкий и понятный API.
Боюсь, я этого не утверждал...
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;JoshuaLight Автор
12.04.2019 12:14заказы всё
Корректно: заказы все. Это значит — все заказы. Что это не вызов
LINQ
выведется из контекста.
UPD: для nullable есть простая старая конструкция:
int? x = null;
int value = x ?? 10;Она тоже неплоха и лаконична, хотя и не так читаема, как
x.Or(10)
.MonkAlex
12.04.2019 12:17Рассчитывать на контекст — плохая идея. Апи должно быть читабельным даже без него.
Кстати, что скажете насчет File.ReadLines и File.ReadAllLines? У меня к этим методам давний вопрос, как можно было сделать апи максимально неочевидным =)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); }
IvanNochnoy
12.04.2019 12:27Если посмотреть на вызов вроде items.ExceptWith(other), не сразу сообразишь, что происходит. Но стоит только написать: items.Exclude(other), как всё становится на свои места.
Нет, не становится: items.ExceptWith(other) на взгляд означает: вернуть items за исключением other, тогда как items.Exclude(other) на взгляд означает: исключить other из items (т. е. изменить items).
JoshuaLight Автор
12.04.2019 12:48items.ExceptWith(other) на взгляд означает: вернуть items за исключением other,
Проблема в том, что
ExceptWith
ничего не возвращает.
IvanNochnoy
12.04.2019 13:03string 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", "")?
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
.
IvanNochnoy
12.04.2019 13:31Заменить можно на: a.Or(b).IfLess() — взять a или b, если a меньше.
Опять нет, читается как: a или b, если меньше. Что именно меньше?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);
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; }
pankraty
13.04.2019 08:21theCreature.Health = (theCreature.Health - damage).ButNotLess(than: 0).However().IfAndOnlyIf(theCreature is (Terribly)Lucky).Then(theCreature.Health + 2 * damage);
JoshuaLight Автор
13.04.2019 10:36Не уверен, иронизируете вы или нет, но код действительно понятный и читаемый (хотя и непрактичный). Лучше в десятки раз, чем любые
Utils
.
eefadeev
12.04.2019 13:34Стойкое ощущение что автор предлагает тараканов из голов одних разработчиков (которые предложили те или иные подходы) заменить на тараканов из своей головы. Но беда в том, что тараканы от этого не перестают быть тараканами (что прекрасно подтверждают комментарии к статье).
Можно было бы тоже начать критиковать конкретные предложения из статьи (а многое из предложенных конкретных примеров и правда — очень плохо), но в этом нет никакого прикладного смысла. Ибо изначальная-то идея совершенно здравая — много думать над понятностью кода. А вот с реализацией — как в той картинке с программистом «Теперь-то я сделаю всё правильно!». Но это объясняется тем, что сложность, в целом, никуда нельзя убрать из индустрии разработки ПО. Это действительно сложно.
IvanNochnoy
12.04.2019 14:02Чувствую себя сукой, но я просто вначле читаю Ваш код, а уже затем комментарии к нему, чесслово.
// Теперь понятно. var x = text.PascalCaseWords().Separated(with: " ");
Мне понятно так: выбрать все слова в Паскалевском стиле и объеденить их пробелами.JoshuaLight Автор
12.04.2019 15:02Мне понятно так: выбрать все слова в Паскалевском стиле и объеденить их пробелами.
Вы всё верно поняли.
IvanNochnoy
12.04.2019 15:22В моем понимании «HelloWorld».PascalCaseWords().Separated(with: " "); вернет «HelloWorld», а у Вас он должен вернуть «Hello World».
Quilin
13.04.2019 12:52Во всем этом семантичном и текстовом коде есть главная проблема. Программисты все сплошь в лучшем случае б2 (конечно, за исключениями), и поскольку кроме как на галеры английский язык на собесах не спрашивают (да и там не парятся), получаются вот эти перлы типа separated with вместо by.
Вот мы например по этой причине отказались от английского геркина в bdd и перешли на русский.JoshuaLight Автор
13.04.2019 14:17Программисты все сплошь в лучшем случае б2
Из этого не следует, что код должен быть некрасивым, неаккуратным, избыточным и многословным.
получаются вот эти перлы типа separated with вместо by
Это несущественно, но точнее, если не ошибаюсь, как раз
with
, а неby
. Пожалуйста.
IvanNochnoy
12.04.2019 14:36// `using static` всё выровнял! Читается прекрасно. var x = items.All(Weapons);
Учитывая, что System.Linq.IEnumerable имеет аналогичный метод, читается как… ну, Вы поняли?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
есть свои нюансы (нужно, чтобы не было коллизий в именах), но, тем не менее, это инструмент!
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 ", ")
JoshuaLight Автор
12.04.2019 19:21Главное достоинство — программист пишет так, как ему удобно думать. Главный недостаток — его коллегам удобно думать по-другому.
Поэтому, как мне видится, в качестве судьи нужно использовать язык. Например,
string.Join(", ", numbers)
можно прочитать разве что как "присоединить к строке запятую и числа", но разве так можно сказать? Да и зачем, если уже есть выражение "separate with a comma"?
Разумеется, о том, какие предложения языка считать более понятными, а какие менее — можно отдельно спорить. Но хорошо бы хотя бы начать вести такие споры. Ведь суть того же примера со
string.Join
не в самомstring.Join
, а в том, что над словом нужно думать, искать, подбирать (что также демонстрирует пример сExceptWith
). А то подчас простой по содержанию код похож на философский трактат с его интенциями, трансцендентностями и прочим.pankraty
12.04.2019 20:27Вот хоть убейте, Separate должен возвращать нечто разделенное (аналог string.Split). А у вас возвращается строка, склеенная из частей. Если бы я разбирался в АПИ незнакомой библиотеки, я искал бы concat, combine, join, glue, aggregate или что-то в этом роде. И когда, не найдя, я полез бы в справку и нашел там, что мне надо использовать separate, я подумал бы, что кто-то сошел с ума — или я, или автор библиотеки.
Особенно выразительно смотрится с пустым разделителем. chars.separate(""). AwesomeIvanNochnoy
12.04.2019 22:08Кстати, вот если язык брать как критерий првильности, то как правильно говорить: список, разделенный запятыми, или список, объединенный запятыми?
pankraty
12.04.2019 22:14Когда на входе — коллекция, а на выходе — строка, то это-таки «элементы, соединенные запятыми». Если же наоборот — то можно получить, допустим, список слов, разделенных [в исходной строке] пробелами.
IvanNochnoy
12.04.2019 23:49Так, ясно, Join — это глагол, Separated — это прилагательное, первое — это действие, второе — это результат. Оба варианта правильны (включая вырожденные случаи с пустым разделителем). Имеет ли смысл заменять императивную семантику на декларативную? Я без понятия. Учитывая, что весь .NET написан в императивном стиле, я бы публичные сборки делал так же, а для внутренних нужд можно и декларативно. Главное, чтобы было понятно и однозначно.
pankraty
13.04.2019 07:39Join — это глагол, Separated — это прилагательное, первое — это действие, второе — это результат.
Даже если оба слова сделать активными (join, separate) или пассивными глаголами (joined, separated), суть не изменяется. Первый про объединение, второй про разделение, и действия эти направлены противоположно. Увидеть separated там, где ожидаешь увидеть joined, не ложится в прицнип наименьшего удивления.IvanNochnoy
13.04.2019 11:27Зна?ки препина?ния — элементы письменности, выполняющие вспомогательные функции разделения (выделения) смысловых отрезков текста, предложений, словосочетаний, слов, частей слова, указания на грамматические и логические отношения между словами, указания на коммуникативный тип предложения, его эмоциональную окраску, законченность, а также некоторые иные функции.
С точки зрения непрограммиста и нематематика: «1,2,3,4,5» — это числа разделенные запятми. Обратите внимание, что каким именно образом получилась эта строка (например, в результате объединения коллеции чисел) неважно. Результат один и то же — это CSV.mayorovp
15.04.2019 14:41С точки зрения непрограммиста типов данных не существует, и число 123 и строка «123» — это одно и то же. А в программировании это не так.
IvanNochnoy
13.04.2019 12:34По поводу пустого рзделителя. Я понимаю, что фраза: "«7956728893045501» — это числа, разделенные пустой строкой" звучит странно, но тем не мение это правда (допустим, это номер кредитной карты).
JoshuaLight Автор
13.04.2019 00:12Вот хоть убейте, Separate должен возвращать нечто разделенное
Если вызывать его на чём-то целом, да. Но когда у вас уже есть нечто разделённое,
Separated(with: ", ")
, как мне кажется, вполне однозначно говорит, что произойдёт. Не ожидаю же я, что мы разделим уже разделённое?
Кстати, именованный параметр
with
обязателен. Сделать его таковым язык не позволяет, но без него конструкция неполна.pankraty
13.04.2019 07:49Если вызывать его на чём-то целом, да. Но когда у вас уже есть нечто разделённое, Separated(with: ", ")...
… как мне кажется, вызывает ступор и InvalidOperationException в мозгу.
SQL шел (идет?) по пути приближения к человеческому языку. Результат, как минимум, спорный, но и там не стали для объединения строк использовать ключевое словоSEPARATED BY
. Назвать функциюSTRING_AGG
, думаю, тоже не было идеальным решением, но она хотя бы образована от AGGREGATE, не SEPARATE.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 с императивного мышления на декларативное, где нет инструкций, а есть описания.
Pand5461
12.04.2019 22:36Разумеется, о том, какие предложения языка считать более понятными, а какие менее — можно отдельно спорить. Но хорошо бы хотя бы начать вести такие споры.
Тут я согласен. Поэтому поспорю :)
Я не уверен, что проблема решается без создания нового языка программирования (который, естественно, имеет все шансы не взлететь, а также все шансы превратить проблему в "теперь у нас n+1 язык, который разработчику в наши времена всяко нужно знать"). С точки зрения естественного языка, например, одна из проблем — это то, что действия могут быть без субъекта и с субъектом. И первые логично бы записывать в префиксной нотации и называть в повелительном наклонении, а вторые — в постфиксной. "Чисто объектные" языки не позволяют одно, "чисто функциональные" — другое, а на смешанных почему-то оказывается написано очень много спагетти-кода. Также есть легаси — программисты банально привыкли, что вызов метода идёт через точку от объекта, хотя это, в общем-то, и необязательно.
Другое решение — разрешить программисту лепить ad hoc конструкции и делать свои DSL для отдельных частей кодовой базы. Но тут из примеров только Лиспы, где с помощью макросов можно "исправить" практически любую "неудачную" конструкцию. Нужно, например, много работать с массивами, а
(aref array index)
писать всё время долго — не проблема. Оборачиваем код в макрос и можем вместо этого писать просто(array index)
. Но из не-лиспов я такого сорта макросы видел только в Julia, и там "человеческая" запись выражений только мешает их писать.JoshuaLight Автор
13.04.2019 00:23Я не уверен, что проблема решается без создания нового языка программирования (который, естественно, имеет все шансы не взлететь, а также все шансы превратить проблему в "теперь у нас n+1 язык, который разработчику в наши времена всяко нужно знать"). С точки зрения естественного языка, например, одна из проблем — это то, что действия могут быть без субъекта и с субъектом. И первые логично бы записывать в префиксной нотации и называть в повелительном наклонении, а вторые — в постфиксной. "Чисто объектные" языки не позволяют одно, "чисто функциональные" — другое, а на смешанных почему-то оказывается написано очень много спагетти-кода. Также есть легаси — программисты банально привыкли, что вызов метода идёт через точку от объекта, хотя это, в общем-то, и необязательно.
Как мне кажется, C# в этом смысле пока сильнее многих других языков: он позволяет создавать и субъектные выражения с помощью методов расширений, и безсубъектные с помощью
using static
. Кроме этого, он разрешает именованные параметры, а это также многократно улучшает читаемость и fluent-составляющую многих вызовов. Но мы всё ещё на этапе хелперов.
Другое решение — разрешить программисту лепить ad hoc конструкции и делать свои DSL для отдельных частей кодовой базы. Но тут из примеров только Лиспы, где с помощью макросов можно "исправить" практически любую "неудачную" конструкцию. Нужно, например, много работать с массивами, а (aref array index) писать всё время долго — не проблема. Оборачиваем код в макрос и можем вместо этого писать просто (array index). Но из не-лиспов я такого сорта макросы видел только в Julia, и там "человеческая" запись выражений только мешает их писать.
Как раз недавно искал какие-то новые языки, которые переосмысливают всю ту императивную кашу, которая осталась ещё с доисторических времён. Понравился Nim. Там кроме элегантного синтаксиса, есть шаблоны и макросы, позволяющие настроить язык под себя, как вы и сказали.
pankraty
13.04.2019 08:13Fluent-подход отлично заходит, когда видишь одну строчку кода, и по ней понятно, что тут происходит. Например, создание маппинга в автомаппере:
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-подхода. Но при многогратном повторении смотрится чище.
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, без натягивания совы на глобус.
IvanNochnoy
12.04.2019 22:16В целом, подход JoshuaLight хорош, но чтобы понять, как разрабатывают API коллеги из Microsoft, рекомендую книгу Цвалина К., Абрамс Б. «Инфраструктура программных проектов: соглашения, идиомы и шаблоны для многократно используемых библиотек .NET». Там подробно написано, почему API .NET именно такое, чем они довольны и в чем раскаиваются.
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), потому что эти методы в проекте вызываются постоянно и однажды плохо написанные, будут всплывать в профайлере постоянно.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
".qw1
14.04.2019 01:37Все аргументы справедливы.
Но лично для меня, конструкция
.Without(".exe").AtEnd;
имеет большие затраты на чтение и работу с ней, чем обычное
Path.GetFileNameWithoutExtension(...)
Я всегда стараюсь понимать до конца, что происходит в каждой строке. Мне не нравится подход «пиши английский текст, и не забивай себе голову тем, что под капотом, библиотека сама тебя поймёт и сделает правильно».
В первом случае больше когнитивная нагрузка, потому что есть два вызова функции, и я должен держать в уме то, что в этой цепочке создаётся искусственный объект, необходимый только для того, чтобы вся эта синтаксическая магия работала.
Во втором случае — обычный вызов статического метода, который делает ровно то, что у него в названии. Никаких лишних сущностей.JoshuaLight Автор
15.04.2019 11:35Мне не нравится подход «пиши английский текст, и не забивай себе голову тем, что под капотом, библиотека сама тебя поймёт и сделает правильно».
Не уверен, что утверждал именно это, предлагая
x.Without(y).AtEnd
.
В первом случае больше когнитивная нагрузка, потому что есть два вызова функции, и я должен держать в уме то, что в этой цепочке создаётся искусственный объект, необходимый только для того, чтобы вся эта синтаксическая магия работала.
Если смотреть на это с такой точки зрения, то да. Поэтому я и предлагаю совершить ментальный скачок (в статьях описывая его суть): смотреть на код не только как на объекты, методы, вызовы, свойства, а как на выражения и предложения.
С такой точки зрения
"Name_postfix".Without("_postfix").AtEnd
читается ровно так, как вы бы ожидали услышать в естественной речи. Или, скажем:
// На мой взгляд, вполне очевидно, что будет в переменной `x`. var x = "D:\\git\\Repository".AllAfter("git\\");
Это может показаться магией, но по тем же причинам магией может показаться управление памятью или даже сама платформа .NET. И тем не менее, мы обобщаем штуки, чтобы писать более красивые и удобные штуки, чтобы потом, обобщая их, писать кое-что ещё лучше.
qw1
15.04.2019 11:56Почему эти идеи встречают неприятие? Они заставляют переучиваться на новый стиль, и неизбежно ухудшать навыки понимания стиля, уже принятого повсеместно. Перейдя на новый стиль, старый код будет раздражать и появится желание всё переписать. Но в то же время, объективных плюсов (как, например, при переходе с C++ на C#) я не вижу, просто другой синтаксис.
Представьте, вас агитириуют писать документацию, комментарии и выбирать названия идентификаторов на немецком вместо английского. И вы вынуждены будете тратить время на адаптацию, на заучивание конструкций, вместо того, чтобы заниматься непосредственно решением задач.
Другое дело, если бы была перспектива — выучу немецкий и код буду писать в 3 раза быстрее. Но нет же — те же яйца в другой профиль. А ещё все вокруг пишут на английском, но вам нужно ваш код писать на немецком.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 раза быстрее. Но нет же — те же яйца в другой профиль. А ещё все вокруг пишут на английском, но вам нужно ваш код писать на немецком.
На мой взгляд, разница утрирована. Куда точнее: "Все вокруг пишут философские трактаты, а нужно, оказывается, писать понятно и просто? Зачем?".
qw1
15.04.2019 13:56+1Это не просто и понятно, это какой-то ребус…
Вот реально,
fileName.Without(".exe").AtEnd
для меня читается как
Скрытый текст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
. Кроме того, суффикс — это понятие слова, а не целой строки.qw1
15.04.2019 14:35+1Непонятен ваш критерий. Мой критерий — английский язык
Критерий — минимальная сложность. Конструкция fileName.Without логически незакончена, к ней нужна одна функция для завершения.
Но fileName не меняется после вызова Remove
Получается название в функциональном стиле, где результат это применение функции ко входу, а вход не меняется )))
Кроме того, суффикс — это понятие слова, а не целой строки.
В computer science, суффикс это любая подстрока, начинающаяся с некоторой позиции до конца строки (см. суффиксное дерево, например).JoshuaLight Автор
15.04.2019 16:20-1Критерий — минимальная сложность. Конструкция fileName.Without логически незакончена, к ней нужна одна функция для завершения.
Из этого не следует, что она сложнее. Для
LINQ
запросов тоже нужны функции для завершения, и, тем не менее, они не являются сложными.mayorovp
15.04.2019 17:11+1В LINQ-запросах промежуточные результаты являются осмысленными, и не требуют специальных «конструкций для завершения».
JoshuaLight Автор
15.04.2019 16:56В computer science, суффикс это любая подстрока, начинающаяся с некоторой позиции до конца строки (см. суффиксное дерево, например).
Кстати, верно, этого я не знал, прошу прощения!
Тем не менее, сравните сложность слов
Remove
иSuffix
иWithout
иEnd
(At
мы в расчёт не берём). В первом случае вам нужно привлекать специалиста по computer science, а во втором — не обязательно. При этом мы, разумеется, ещё учитываем почти полную тождественностьWithout(x).AtEnd
тому, как это произносится в речи.
Вот то самое стремление к минимальной сложности (т.е. к упрощению) и получается.
qw1
15.04.2019 17:34Я считаю сложность не по длине идентификаторов (тут auto-complete помогает), а по количеству синтаксических единиц. RemoveSuffix — один токен, Without+AtEnd+лишняя точка — три токена.
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")
Обычно пользователь сначала выбирает файлы, а потом действие.
JoshuaLight Автор
15.04.2019 11:08Обычно пользователь сначала выбирает файлы, а потом действие.
Но говорим-то мы "copy files".
qw1
15.04.2019 11:40Возращаемся обратно к
CopyUtil.Copy(files);
или, используя языковые возможности C# 6,
using static CopyUtil;
Copy(files);
(если не нравится указывать префикс класса)JoshuaLight Автор
15.04.2019 11:43CopyUtil.Copy(files);
Потерялась папка.
Я бы предложил тогда:
Copy.Files(from: source, to: destination);
Зачем
CopyUtil
? Что это такое?qw1
15.04.2019 12:05Стиль мышления технаря против стиля мышления гуманитария.
Технарь — аналитик, он языковые конструкции расщепляет на составляющие, и у каждой должен быть свой смысл, отдельный, самодостаточный.
Класс Copy — это что? Копия? Нет, это класс, выполняющий копирование, т.е. копировщик (Copier, например).
Метод Files — это что? В отрыве от имени класса вообще бред.
Т.е. для аналитического типа мышления это категорически не заходит. Читать код как поэму, на одном дыхании — увы, не ко мне.JoshuaLight Автор
15.04.2019 12:56Стиль мышления технаря против стиля мышления гуманитария.
Мне кажется, это придуманное, а не фактическое положение вещей.
Что технического в
CopyUtil
?
Нет, это класс, выполняющий копирование
Это и есть копирование.
В отрыве от имени класса вообще бред.
Так в этом и суть: сущности не самодостаточны, а читаются и используются. Как технари мы боремся против сложности и избыточности, стремимся к простоте и понятности.
Мало написать и реализовать алгоритм — это ничто по сравнению с тем, чтобы написать его доступным для других.
qw1
15.04.2019 13:58+1Это и есть копирование.
Сразу видно, вы не математик и в формализмы не умеете. Потому что формально, что CopyUtil, что ваш класс Copy — это класс, который… и далее по тексту.
Deosis
16.04.2019 09:01+2То есть, вы привязываете интерфейс к английскому языку.
Если, например, на хинди порядок слов в предложении другой, то индусам надо выдать принципиально другой API?
tuxi
как по мне, первый вариант GetValueOrDefault более читабельный, можно сократить до GetOrDefault. На хешмапах начиная java8 так и сделали кстати
JoshuaLight Автор
А потом, следуя советам второй статьи, мы убираем
Get
, и получаетсяOrDefault
— лаконично и просто, без излишеств.tuxi
У хешмапов есть еще putIfAbsent, put тоже будем убирать? :)
Неее, именно .GetOrDefault(key, 0) отлично ложится и читается
JoshuaLight Автор
Put
не будем, потому что это действие, и его мы хотим отразить.Get
будем, потому что это не действие, и его мы не хотим отражать.tuxi
JoshuaLight Автор
Не совсем понимаю, какое состояние меняется.
Вот есть словарь, а вот есть значение с ключом x. Что получается и что меняется?
tuxi
Get это тоже глагол как и Put и это тоже действие. Но это действие не изменяет состояние/содержимое хэшмапа. Я и уточнил, что подразумевается под «это не действие»
JoshuaLight Автор
Я подразумевал такую штуку: если нужно показать действие (
Put
), то мы используем глагол; если нужно показать просто какую-то вещь как часть другой — не используем.Можете полагать значение с ключом x как бы свойством хешмапы, как, например, у яблока есть свойство вес.
Знаю, в Java всё равно пишут
getWeight()
, но, мне кажется, это кто-то зря придумал и очень-очень давно. Лучшеweight()
. Потому что вес — это не действие, которое яблоко умеет делать, а его свойство.Simplifier
Почему «положить в» — это действие, а «вытащить из» — уже не действие?
JoshuaLight Автор
Потому что "вытащить из" — промежуточный этап, который не так важен, как его результат.
Когда я кладу элемент в коллекцию
Put
, мне жизненно необходимо понимать, что нечто случится. Действие — конечная цель.Когда мне нужен элемент коллекции по ключу, то не имеет значения, достаётся он, получается, и т.д. Даже если он достаётся из базы, он нужен всё равно. Элемент — конечная цель.
IvanNochnoy
В .NET есть совершенно четкая разница между свойством (без префикса Get, ессно) и методом: свойство выполняется мгновенно, а выполнение метода может быть длительным. Если разработчик видит Get, он сразу понимает, что полученное значение желательно сохранить в локальной переменной. GetValueOrDefault потенциально может быть длительным, так как неизвестно, сколько этот самый Default будет создаваться, поэтому Get опускать нельзя.
JoshuaLight Автор
Боюсь, чёткость этой разницы крайне сомнительна, а критерий производительности устарел. Советую взглянуть на вторую часть статьи: в большинстве случаев важна семантика, а не конкретные технические детали.
IvanNochnoy
JoshuaLight Автор
Рекомендация сама по себе неплохая, но её слабость в следующем утверждении:
Это неверно.
Поэтому мы видим:
Where
,Select
и т.д.;Enumerable.Empty<T>()
;ImmutableArray.As<T>()
.IvanNochnoy
Where, Select выполняются мгновенно, так как вычисления LINQ-выражений ленивые;
Enumerable.Empty() возвращает константу;
ImmutableArray.As() — здесь итак понятно, что время выполнения прямо пропорционально размеру массива;
В любом случае, чтобы нарушить рекомендации нужны веские основания.
mkshma
Не убираем, потому что становится совсем не очевидно что функция вообще делает без предварительного ее просмотра. Избыточность не всегда плохо.