Pull to refresh
17
0
Джошуа Лайт @JoshuaLight

Software Developer

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

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


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

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


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


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


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

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


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

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

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


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

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

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


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

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


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

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


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


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


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


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


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


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


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


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

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

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

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

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

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


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

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

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


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

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


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


Python


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

Java


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

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


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

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


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

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

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

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


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

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


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

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


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


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

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

Я тоже.

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

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


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


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

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

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

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

Непонятен ваш критерий. Мой критерий — английский язык. Неужели, если в коде написано 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. Кроме того, суффикс — это понятие слова, а не целой строки.

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

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


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

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

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

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


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


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

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


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

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


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

CopyUtil.Copy(files);

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


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


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

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

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

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

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


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

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


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


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

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

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

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

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

А вот же.


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

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


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

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


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

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


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

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


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


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


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

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

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


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

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

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

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


Поэтому не просто Directory.Enumerate, а EnumerateFiles и EnumerateDirectories, и вообще, глагол+существительное как стандарт.

Зачем так, если можно хотя бы Directories.Of(path) и Files.Of(path)? Глагол не нужен совершенно. При этом лучше вообще использовать методы расширений, чтобы передать "субъектный" оттенок: path.AsDirectory().Files и path.AsDirectory().Directories. Тогда и расширяемость выше.

Потому что это неверно. Метод не ищет свободный поток.

Это потому что вы знаете, как устроен ThreadPool внутри. А метод, меж тем, не должен сообщать, что он делает внутри.


Мне, как клиенту ThreadPool.QueueUserWorkItem совершенно безразлично, будет там внутри очередь или Scheduler. У меня есть действие, и я хочу выполнить его на потоке из пула. Поэтому: "Пул, подыщи-ка мне поток, и выполни эту задачу" — вот вам и FindFreeThreadAndExecuteUserWorkItem получился.


Если в имя метода добавить «Find», может показаться, что от наличия свободного потока зависит результат (если такого нет, метод вернёт false, например).

Если прочитать только Find, но там есть ещё AndExecute.


В общем, на мой взгляд, всё это несущественные споры. Основная претензия была не Queue (хотя и к нему есть), а к UserWorkItem.


Как вам ThreadPool.Queue?

Information

Rating
Does not participate
Location
Харьков, Харьковская обл., Украина
Date of birth
Registered
Activity