В прошлый раз я писал о том, что имена объектов имеют большое значение, и что подбирать их нужно кропотливо и со вниманием к деталям. Плохое имя отпугивает и не даёт вникнуть в суть происходящего. Но что это за суть?
Сложно оценить героя, не поняв его "статы" и "абилки". Что он может и на что способен — вот следующий уровень сложности, на который нам придётся нырнуть. Мало с помощью точного имени отразить внутреннее святилище объекта, ещё следует убедиться, что это таки святилище, а не конюшни из геттеров.
Об этом — в статье.
Оглавление цикла
- Объекты
- Действия и свойства
- Код как текст
Действия
Персонаж атакует, защищается, уворачивается, стреляет из лука, использует заклинания, взмахивает клинком. Имя отражает объект, но сам объект — в движении, в реакции, в действиях. В противном случае мы бы говорили о таблицах в Excel.
В C# действия — методы и функции. А для нас: глаголы, атомы словесного движения. Глаголы двигают время, из-за них объекты существуют и взаимодействуют. Где есть изменение — там должен быть глагол.
Сеттеры
Из всех изменений менее всего подвижно присваивание. Оно строго и математически описывает, что есть величины и чему они равны, но никогда не сообщает жизни и бодрости тексту, как это делают глаголы.
Например, есть IPullRequest
со свойством Status
, которое может быть Approved
, Declined
или Merged
. Можно писать pullRequest.Status = Status.Declined
, но это то же самое, что говорить “Установи пул-реквесту отменённый статус”, — императивно. Куда сильнее — pullRequest.Decline()
и, соответственно, pullRequest.Approve()
, pullRequest.Merge()
.
Активный глагол предпочтительнее сеттера, но не все глаголы такие.
Пассивный залог
PerformPurchase
, DoDelete
, MakeCall
.
Как в HeroManager
важное существительное заслоняется бессмысленным Manager
, так и в PerformMigration
— Perform
. Ведь живее — просто Migrate
!
Активные глаголы освежают текст: не “нанёс удар”, а “ударил”; не “сделал замах”, а “замахнулся”; не “принял решение”, а “решил”. Так и в коде: PerformApplication
> Apply
; DoDelete
> Delete
; PerformPurchase
> Purchase
, Buy
. (А вот DealDamage
устоялось, хотя в редких случаях может иметься в виду Attack
.)
Избегая пассивного залога, мы развиваем историю, двигаем персонажей, но ещё нужно проследить, чтобы кино не получилось чёрно-белым.
Сильные глаголы
Некоторые слова лучше передают оттенки смысла, чем другие. Если написать “он выпил стакан воды”, получится просто и понятно. Но “осушил стакан воды” — образнее, сильнее.
Так изменение здоровья игрока можно выразить через player.Health = X
или player.SetHealth
, но живописнее — player.RestoreHealth
.
Или, например, Stack
мы знаем не по Add/Remove
, а по Push/Pop
.
Сильные и активные глаголы насыщают объект поведением, если они не слишком конкретны.
Избыточные детали
Как и с ManualResetEvent
, чем ближе мы подбираемся к техническим внутренностям .NET, которые сложны и хорошо бы их выразить просто, тем насыщеннее подробностями и излишествами получается API.
Бывает, нужно выполнить какую-то работу на другом потоке, но так, чтобы не хлопотать о его создании и остановке. В C# для этого есть ThreadPool
. Только вот простое “выполнение работы“ тут — QueueUserWorkItem
! Что за элемент работы (WorkItem
) и какой он может быть, если не пользовательский (User
), — неясно. Куда проще было бы — ThreadPool.Run
или ThreadPool.Execute
.
Другой пример. Помнить и знать, что есть атомарная инструкция compare-and-swap (CAS) — хорошо, но переносить её подчистую в код — не самое лучшее решение. Interlocked.CompareExchange(ref x, newX, oldX)
во всём уступает записи Atomically.Change(ref x, from: oldX, to: newX)
(с использованием именованных параметров).
Код — не докторская по работе с квантовым компьютером, не приложение к математическим выкладкам, а читателю подчас совершенно безразлично, как называются низкоуровневые инструкции. Важно повседневное использование.
Повторения
UsersRepository.AddUser
, Benchmark.ExecuteBenchmark
, AppInitializer.Initialize
, UniversalMarshaller.Marshal
, Logger.LogError
.
Как и говорил в прошлой части, повторения размывают смысл, ужимают пространство.
Не UsersRepository.AddUser
, а UsersRepository.Add
; не Directory.CreateDirectory
, а Directory.Create
; не HttpWebResponse.GetResponseStream
, а HttpWebResponse.Stream
; не Logger.LogError
, а Log.Error
.
Мелкий сор
Check
— многоликое слово. CheckHasLongName
может как возвращать bool
, так и бросать исключение в случае, если у пользователя слишком длинное имя. Лучше — bool HasLongName
или void EnsureHasShortName
. Мне даже встречался CheckRebootCounter
, который… Где-то внутри перезагружал IIS!
Enumerate
— из той же серии. В .NET есть метод Directory.EnumerateDirectories(path)
: зачем-то уточняется, что папки будут перечисляться, хотя проще ведь Directories.Of(path)
или path.Directories()
.
Calc
— так часто сокращают Calculate
, хотя больше смахивает на залежи кальция.
Proc
— ещё одно причудливое сокращение от Process
.
Base
, Impl
, Internal
, Raw
— слова-паразиты, указывающие на переусложнённость объектов.
Итого
Вновь, заметит внимательный читатель, всё сводится к упрощению, к уподоблению естественной речи, да и сами советы во многом касаются не только кода, а письма вообще. Пользуясь ими, разработчик шлифует и код как текст, и сам текст, стремясь к прозрачному, гладкому изложению, к простоте.
Теперь, разобравшись с движением и “спецэффектами”, посмотрим на то, как описываются отношения между объектами.
Свойства
У персонажа есть здоровье и мана; в корзине покупок находятся предметы; солнечная система состоит из планет. Объекты не только самозабвенно действуют, но и соотносятся: иерархически (предок-наследник), композиционно (целое-часть), пространственно (хранилище-элемент) и т.д.
В C# свойства и отношения — методы (как правило начинающиеся с Get
), геттеры (свойства с определённым телом get
) и поля. Для нас же это: слова-дополнения, выражающие принадлежность одного объекта другому. Например, у игрока есть здоровье — Player.Health
, что почти точно соответствует английскому “player’s health“.
Больше всего нынче путают методы-действия и методы-свойства.
Глагол вместо существительного
GetDiscount
, CalculateDamage
, FetchResult
, ComputeFov
, CreateMap
.
Отовсюду слышно устоявшееся: методы должны начинаться с глаголов. Редко встретишь, чтобы кто-то засомневался: а точно ли это так? Ведь не может быть, чтобы между Player.Health
и Player.Health()
была существенная разница. Пусть записи синтаксически отличаются, подразумевают они одно и то же.
Положим, в IUsersRepository
легко ожидается какой-нибудь GetUser(int id)
. Отчего для представления пользователя додумывать какое-то получение (Get
)? Аккуратнее будет — User(int id)
!
И действительно: не FetchResult()
, а Result()
; не GetResponse()
, а Response()
; не CalculateDamage()
, а Damage()
.
В одном докладе по DDD дают пример “хорошего” кода: DiscountCalculator
с методом CalculateDiscountBy(int customerId)
. Мало того, что на лицо симметричный повтор — DiscountCalculator.CalculateDiscount
, так ещё и уточнили, что скидка вычисляется. А что ещё с ней, спрашивается, делать?
Сильнее было бы пойти от самой сущности — Discount
с методом static decimal Of(Customer customer, Order order)
, чтобы вызывать Discount.Of(customer, order)
— проще, чем _discountCalculator.CalculateDiscountBy(customerId)
, и соответствует единому языку.
Иногда же, опустив глагол, мы кое-что теряем, как, скажем, в CreateMap()
: прямой замены на Map()
может быть мало. Тогда лучшее решение — NewMap()
: снова во главе объект, а не действие.
Использование пустопорожних глаголов свойственно устаревшей, императивной культуре, где алгоритм первичен и стоит впереди понятия. Там чаще встретишь “клинок, который закалили”, чем “закалённый клинок”. Но стиль из книг про Джеймса Бонда не подходит для описания пейзажа. Где нет движения, там глаголу не место.
Другое
Свойства и методы, выражающие отношения между объектами, — тоже объекты, поэтому сказанное выше во многом относится и к ним.
Например, повторения в свойствах: не Thread.CurrentThread
, а Thread.Current
; не Inventory.InventoryItems
, а Inventory.Items
, и т.д.
Итого
Простые, понятные слова не путают, и поэтому код, состоящий из них, также не путает. В писательском мастерстве не менее важно писать легко: избегать пассивных залогов, обилия наречий и прилагательных, повторений, для действий предпочитать глагол существительному. Общеизвестный пример: “Он кивнул своей головой, соглашаясь” вместо “Он кивнул” вызывает улыбку, и вспоминается QueueUserWorkItem
.
Текст от кода отличается ещё тем, что в первом случае вам заплатят, если дом стоит, утопая в лучах заходящего солнца; во втором — если дом стоит; но стоит помнить: стоять должен дом, а не палки из хелперов.
В первых двух статьях цикла я хотел показать, как важно работать не только над алгоритмом, но и словом; как названия определяют содержание называемого; как избыточный и переусложнённый код отгоняет читателя.
Вместе с этим, хорошие имена — только ноты. Чтобы заиграть, они должны стать написанными и воплотиться в музыке. Расскажу подробнее в следующей, заключительной статье.
Комментарии (62)
dopusteam
11.04.2019 13:29Почему IUsersRepository.User(int Id) вместо IUsersRepository.Get(int Id) по аналогии с add?
JoshuaLight Автор
11.04.2019 14:34Потому что
Get
— это глагол, а никакого значимого действия мы указывать не хотим, только то, что вот пользователь с идентификатором ID.
Но и
User
— не предел. Я улучшу это имя, развив ряд соображений из этой статьи, в статье следующей.dopusteam
11.04.2019 16:16Мне кажется немного притянуто получается.
Почему жеа никакого значимого действия мы указывать не хотим
?
Я хочу именно что указать, что мне нужно получить пользователя, разве нет?
Просто в Вашем случае интерфейс получается примерно такой
Add()
Update()
User()
т.е. неконсистентный, что по мне гораздо хуже для читабельности.
Но посмотрим, что будет в следующей статьеJoshuaLight Автор
11.04.2019 16:21Я хочу именно что указать, что мне нужно получить пользователя, разве нет?
В том то и дело, что вот это "получить" — лишнее, ненужное. Получить пользователя и пользователь подразумевают одно и то же (пользователя), только первое зачем-то что-то уточняет.
dopusteam
11.04.2019 19:54+3Но что в итоге? IUsersRepository.User vs IUsersRepository.Get
Дублирование vs практически предложение со смыслом
mayorovp
12.04.2019 15:18А почему вы решили не указывать никакого значимого действия, когда оно есть?
Загрузка объекта из хранилища — тоже действие, притом дорогое (по сравнению с работой с объектами в памяти).JoshuaLight Автор
12.04.2019 17:36-1Загрузка объекта из хранилища — тоже действие, притом дорогое (по сравнению с работой с объектами в памяти).
Загрузка — это
Load
, а неGet
.
Кстати, даже тут можно уйти от
Load
. По контексту хранилища обычно понятно, будет это дорогостоящая операция или нет. Например,database.User(id)
— мы видим, что обращениеUser
происходит на объекте базы, поэтому понимаем: операция дорогостоящая.
Кроме этого, если контракт (интерфейс) обобщает способ получения и, вообще, конкретные детали, то уточнение стоимости получения — нарушение инкапсуляции.
Ketovdk
11.04.2019 14:25Сильнее было бы пойти от самой сущности — Discount с методом static decimal Of(Customer customer, Order order), чтобы вызывать Discount.Of(customer, order) — проще, чем _discountCalculator.CalculateDiscountBy(customerId)
А как этот статический класс инджектить в IoC — container? DDD часто используется в вебе, где без этого никуда (да и не только в вебе без этого никуда)JoshuaLight Автор
11.04.2019 14:44Если нужно инжектить, то пожалуйста:
public interface IDiscount { decimal Of(Customer customer, Order order); }
Вызов тогда:
var discount = _discount.Of(customer, order); // Но никак не: var discount = _discountCalculator.CalculateDiscountBy(customerId, order);
Суть осталась та же, изменилась только форма.
ksbes
11.04.2019 15:49Ну а если нам нужно ещё и устанавливать/сбрасывать скидку + плюс вычислять не только скидку, но и, скажем, НДС со скидкой?
Т.е. в достаточно сложном коде появляются односмысленные (но принципиально разные) действия (вычисление/установка и т.д.) над разнотипными объектами. А если сверху это всё и иерархией с дженериками накрыть — так вообще уже через несколько часов сам автор перестанет понимать .of() чего он делает.
А так у нас есть CalculatDiscount, CalculateNDS (о, ужас, я знаю — просто не помню как это по английски — тоже проблема, кстати), CalculateTotalSum и т.д. И уже код читается почти как английский текст и от точек в глазах не рябит.JoshuaLight Автор
11.04.2019 16:13Я бы не хотел отходить от темы, размышляя "а что если". Боюсь, тогда мы тут надолго застрянем.
Совершенно точно, что простое
IDiscount
имеет свои пределы, но пределы эти — часть другого обсуждения, мне же оно нужно было для того, чтобы показать идею.
Кстати, если взять
CalculateDiscount()
,CalculateNDS()
иCalculateTotalSum()
, то проще —Discount()
,NDS()
иTotalSum()
. Что оноCalculate
, и так понятно.ksbes
11.04.2019 16:26Не всегда понятно, когда работа идёт с этими методами внутри класса, где, скорее всего уже есть приватные или локальные переменные с такими названиями. Теряется читаемость.
К тому же английский язык бывает коварен. TotalSum() может и понятен, а вот просто Sum() — уже нет. Это глагол (меняющий внутреннее состояние объекта) или существительное? Тоже самое: Add это совсем не то же самое что Push (в первом случае непонятно что в конец).
Как вы должно быть поняли — пишу с точки зрения джависта и джавовский гайдлайнов, где это вопрос именования расписан и стандартный вариант именно глагол-существительное (что и над чем).JoshuaLight Автор
11.04.2019 16:57Не всегда понятно, когда работа идёт с этими методами внутри класса, где, скорее всего уже есть приватные или локальные переменные с такими названиями.
Да, существуют ситуации, когда уже есть какие-то переменные или поля с такими же именами. Но и их можно обойти, всё зависит от конкретного случая, главное понимать, зачем глагол, а зачем существительное. В случае с
Calculate
глагол уж точно ни к чему.
Как вы должно быть поняли — пишу с точки зрения джависта и джавовский гайдлайнов, где это вопрос именования расписан и стандартный вариант именно глагол-существительное (что и над чем).
В C# для методов существует такое же правило. Интересно, что если метод, то
Get
(или какой-нибудьCalculate
) пишется, а вот если свойство (синтаксически это метод без параметров и без скобочек) — то нет. Как если бы от физического воплощения (свойство или метод) одного и того же понятия мы бы по разному говорили о нём между собой.
Думается мне, критерий всегда один: говорят "скидка", значит
Discount
, и т.д., тут в комментарии ниже описывал.
Код тогда строится не по действиям, императивно, а описательно: не "скидка считается считателем скидок по формуле", а "скидка — это...". Разумеется, там где действие, там остаётся царствие глагола.
Simplifier
12.04.2019 11:45Интересно, что если метод, то Get (или какой-нибудь Calculate) пишется, а вот если свойство (синтаксически это метод без параметров и без скобочек) — то нет. Как если бы от физического воплощения (свойство или метод) одного и того же понятия мы бы по разному говорили о нём между собой
Ну да, это потому что в случае свойства действие (гет или сет) уже передается синтаксисом и дублировать его словесно не надо. order.Discount = 5 — свойство слева от присваивания, поэтому сет.
К тому же у метода множество различных возможных вариантов действия, а у свойства только два, поэтому нет неоднозначности, в отличие от методаJoshuaLight Автор
12.04.2019 11:47"гет" — это не действие.
Действие это или нет, различается не синтаксически и формально (метод или свойство), а семантически: скидка это, пользователь из базы, запись, добавление.
Ketovdk
11.04.2019 16:07Ну, название интерфейса IDiscount очевидно не хорошее, с точки зрения естественного языка, так как не передает совершаемого им действия (ну он не Я скидка, а Я считаю скидку), так-что в конечном итоге его бы пришлось назвать IDiscountCalculator или ICalcDiscount и вернуться ко второму варианту, потому-что _discountCalculator.Of(customer) опять же не правильно с точки зрения языка. Потому-что если перевести, то получается, что он должен возвращать инстанс какого-то калькулятора для конкретного пользователя, а не считать скидку
JoshuaLight Автор
11.04.2019 16:15Ну, название интерфейса IDiscount очевидно не хорошее, с точки зрения естественного языка, так как не передает совершаемого им действия
Совершаемое действие не имеет значения. Имеет значения результирующее понятие.
qw1
11.04.2019 22:50Потом окажется, что Discount это не числовое значение, а сущность с несколькими полями. Захочешь назвать её «Discount», а это имя уже занято сервисом-калькулятором.
JoshuaLight Автор
12.04.2019 11:36Думаю, просто следует проявить изобретательность.
public interface IDiscount { (decimal Value, decimal AdditionalValue) Of(Customer customer, Order order); }
Всегда можно поиграться и найти то, что подходит. Если на каждое улучшение находится "если", это не значит, что нужно вообще ничего не улучшать и писать повсюду:
_discountCalculator.CalculateDiscount
(ещё утверждается, что это DDD).qw1
12.04.2019 20:00Нафиг-нафиг. Кортежи, безымянные классы — всё это тяжко рефакторить.
Попробуйте поискать FindUsages такого AdditionalValue — найдутся обращения к Item2 всех кортежей.JoshuaLight Автор
12.04.2019 23:55Попробуйте поискать FindUsages такого AdditionalValue — найдутся обращения к Item2 всех кортежей.
И вот мы начинаем обсуждать, хороши ли именованные кортежи, как часто их приходится рефакторить, искать использования полей...
Изначальный посыл: использовать понятия из предметной области, избегать повторений. В предметной области вычислителей скидок нет, а
_discountCalculator.CalculateDiscount
содержит словаdiscount
иcalculator
по два раза. Поэтому нужно думать.
Кстати, есть ещё вариант:
public interface IDiscount { void Apply(Customer customer, Order order); }
Но и тут можно придраться к тому, менять
Order
нельзя. Как и к тому, что скидка с несколькими полями — вещь сомнительная. Разумеется,Apply
можно переделать наApply(ref price, Customer customer, Order order)
, но и тут можно найти минусы.
Без контекста все эти конкретные размышления парируются каким-нибудь "а что если", при этом изначальный посыл от этого не меняется.
qw1
13.04.2019 00:54Кстати, есть ещё вариант
Плохой, потому что опять не решает проблему управления скидками как сущностями. После модификации Order этим методом теряется информация, сколько заказ стоил до скидки. Также легко себе представить справочник скидок, где у скидки есть сроки действия, или они подключаются списком детализации к строке заказа. Тут и вырисовывается DiscountManager как сервис, и Discount как сущности, с которыми он работает.
Как и к тому, что скидка с несколькими полями — вещь сомнительная
Вовсе нет, потому что типичная скидка в сложных системах — алгебраический тип-сумма, может быть либо процентной, либо абсолютной. Также неплохо бы хранить основание для скидки (постоянному клиенту, или по акции №55).JoshuaLight Автор
13.04.2019 10:22После модификации Order этим методом теряется информация, сколько заказ стоил до скидки
Зависит от реализации.
Тут и вырисовывается DiscountManager как сервис, и Discount как сущности, с которыми он работает.
Боюсь, вы от плохо тогда перешли в чудовищно плохо. Это неподдерживаемый, непонятный код. Применение и подсчёт скидки должен быть в скидке, а не в каком-то менеджере. Всегда можно обойтись без него.
Кстати, вот ещё в голову пришёл вариант:
public interface IDiscounts { IDiscount One(Customer for, Order and); } public interface IDiscount { public int Value { get; } public DiscountType Type { get; } public DateTime WillBeAvailableTo { get; } } // Вызываем так: Order order = ...; Customer customer = ...; IDiscounts discounts = ...; // Инжектим и т.д. var price = Price(of: order); // Как-то считается цена. var discount = discounts.One(for: customer, and: order); discount.Apply(ref price); // `Apply` или внутрь `IDiscount`, или методом расширений.
Даже такой скорый вариант сильнее всякого менеджера или хелпера хотя бы тем, что он не размазывает ответственности. Думаю, можно намного лучше, если иметь хотя бы какое-то представление о конкретной задаче и её особенностях.
qw1
13.04.2019 11:10Кстати, вот ещё в голову пришёл вариант:
Ну, замечательно. Вы изобрели DiscountService/DiscountManager под именем Discounts.
Если вы собираетесь отстаивать своё наименование, то представьте, что ваш коллега Вася из соседнего отдела напилил кучу интерфейсов: IDiscount, IOrder, IOrders, IShop. Что из них репозиторий, что из них сущность, что сервис — непонятно.
Можно экономить на названиях, но через 3 года сам же откроешь свой код, и будешь вспоминать, почемуIOrders
— это репозиторий, аIDiscount
— сервис с методом
void Apply(Customer customer, Order order);
Применение и подсчёт скидки должен быть в скидке, а не в каком-то менеджере
Такой подход убивает абстракцию. Если у нас несколько каналов продаж, можно было бы сделать объект Discount, содержащий лишь информацию о значениях скидок, а их вычисление отдать сервисам, которые будут писать разные люди.
У вас получается, что скидка должна знать про каждый канал продаж, появление нового канала потребует внесения изменений в код Discount. Это нормально, когда пилишь сам себе простой проект, в котором сам всё контролируешь. Но плохо для промышленной разработки.
JoshuaLight Автор
13.04.2019 11:44Ну, замечательно. Вы изобрели DiscountService/DiscountManager под именем Discounts.
Имя — это всё.
Что из них репозиторий, что из них сущность, что сервис — непонятно.
Это избыточные детали. Приложение-то по работе с заказами, а не с сервисами заказов.
Можно экономить на названиях
Но ведь это не экономия на названиях, а использование слов из предметной области.
Можно экономить на названиях, но через 3 года сам же откроешь свой код, и будешь вспоминать, почему IOrders — это репозиторий, а IDiscount — сервис с методом
void Apply(Customer customer, Order order);Единственное, что можно подумать:
IDiscount
— это скидка, аIOrders
— заказы.
Такой подход убивает абстракцию.
Боюсь,
DiscountManager
— это не абстракция...
Если у нас несколько каналов продаж, можно было бы сделать объект Discount, содержащий лишь информацию о значениях скидок, а их вычисление отдать сервисам, которые будут писать разные люди.
Это новое "если".
У вас получается, что скидка должна знать про каждый канал продаж, появление нового канала потребует внесения изменений в код Discount.
Если я верно вас понял, вы предлагаете переиспользовать "лишь информацию о скидке". А если в каждом из каналов скидки выражаются разными данными? А если где-то выдаётся только процентная скидка? Если она не всегда ограничена по времени?
На мой взгляд, вот уж что точно не получится масштабировать и удобно использовать, так это переиспользование информации. Ведь переиспользуются объекты.
qw1
13.04.2019 11:53А если в каждом из каналов скидки выражаются разными данными? А если где-то выдаётся только процентная скидка? Если она не всегда ограничена по времени?
Придётся данные скидки привести к общему знаменателю, под максимальный случай. Ведь, как это часто бывает, захотят видеть агрегированную статистику, а агрегировать можно однородные данные.
alex1t
11.04.2019 14:46Я тоже где-то читал про читаемость кода и там ещё дальше развивали эту идею, делая кучу extension-методов для базовых типов, чтобы можно было написать что-то вроде
Thread.Sleep(5.Minutes())
qw1
11.04.2019 22:555.Minutes()
Как финтифлюшка в HelloWorld забавно смотрится, но не масштабируется для большого проекта.
Потому как вDispatcherTimer(5.Minutes())
Minutes должен вернуть TimeSpan, а в контексте, когда нужны тики (FileTime) — Minutes должен вернуть тоже целое число, но не в миллисекундах, а в других единицах, сотнях наносекунд. И что с этим Minutes делать, когда единственное применение намертво прибито к типу int?alex1t
11.04.2019 23:04Я тоже согласен, что это излишне. Помимо ваших возражений могу даже добавить то, что эти функции будут вылезать на любой int если вы используете пространство имён где объявлены эти расширения
JoshuaLight Автор
12.04.2019 11:25Как финтифлюшка в HelloWorld забавно смотрится, но не масштабируется для большого проекта.
Хм. Сперва постулируется: "не масштабируется для большого проекта", а затем как подтверждение — довольно частный пример, который может касаться лишь одной из подсистем этого самого большого проекта.
Кроме этого, надо понимать,
5.Minutes()
работает там, где работает, а там, где не работает, ищется другое.
Положим, описываю я дебаффы в игре:
Debuff(CriticalChanceDecrease, power: 10, duration: 5.Minutes());
Теперь масштабируем эту строчку в десятки раз, и получаем вполне приличный, читаемый код. Система, которой нужны тики, а также иные, сложные, требующие технической сноровки, не пострадали.
qw1
12.04.2019 20:02Не масштабируется в том смысле, что этот подход нельзя перенести на остальные подсистемы, потому что int.Minutes() уже занято какой-то одной.
Не получается единообразия.JoshuaLight Автор
12.04.2019 23:56что этот подход нельзя перенести на остальные подсистемы
Зачем это может быть нужно? Каждая подсистема имеет свой ограниченный контекст. Жертвовать читаемостью кода из-за того, что он не масштабируется на разработку операционных систем для ракетостроения, — не самое лучшее решение.
qw1
13.04.2019 01:00Рано или поздно захотят интегрировать подсистемы, и придётся в одном модуле оперировать сущностями из разных подсистем. И тут экономия в детализации наименований выйдет боком.
JoshuaLight Автор
13.04.2019 10:33Мне кажется, такие рассуждения о том, что будет или чего не будет, как правило, не заканчиваются успехом. А код читается сейчас.
eugene_bx
11.04.2019 18:34Слышал мнение что у тех программистов для которых английский язык не родной, есть преимущество в том что английские названия для них более абстрактны и не добавляют паразитный фон (даже для тех кто владеет языком очень хорошо).
AlekseyArh
12.04.2019 11:27Подтверждаю. Я один из тех программистов, который считает что язык программирования и литературный язык нужно разделять на корню. Простой пример, который доставлял бы мне дискомфорт в программировании если бы английский язык был бы для меня родной. Door->open() vs Open->door().
JoshuaLight Автор
12.04.2019 11:31Простой пример, который доставлял бы мне дискомфорт в программировании если бы английский язык был бы для меня родной. Door->open() vs Open->door().
Да, в ООП предполагается такое незначительное нарушение грамматики, но из него никак не следует, что нужно идти в диаметрально противоположное направление и писать хелперы.
JoshuaLight Автор
12.04.2019 11:29К сожалению, не могу подтвердить или опровергнуть это мнение. В основном наблюдаю пренебрежение к языку и словам, из-за которого код получается неопрятным, неаккуратным, но зато рабочим, как если бы его работоспособность была конечным продуктом.
Разве что, случается (ведь я сам не носитель): некоторые слова кажутся вычурными и неестественными, потому что редко их встречаешь или до этого не знал.
Wyrd
11.04.2019 22:37+3QueueWorkItem vs Execute — имхо, первое намного лучше: оно даёт понимание того, что ThreadPool исполнит задачу «когда-нибудь», не обязательно сейчас. Execute такого понимания не даёт и это достаточно больная проблема, имхо
JoshuaLight Автор
12.04.2019 11:27На мой взгляд, сама семантика вызова:
ThreadPool.Execute
предполагает, что действие не будет выполнено мгновенно, поскольку мы работаем в контекстеThreadPool
.
Никогда бы не подумал что-то вызвать на тредпуле, не понимая, что это в действительности значит: что там есть потоки, они управляются, что не нужно их надолго задерживать. Тип
ThreadPool
сообщает вполне достаточно.
Но аргумент понятен. Как вам:
ThreadPool.Queue(action)
? ЗачемWorkItem
? Что это значит?Wyrd
12.04.2019 12:10WorkItem это объект доменной модели если можно так выразится. Например, в приложениях под IIS WorkItem не просто становится в очередь и выполняется когда-то, но и также не даёт IIS pool уйти в recycle до тех пор пока он не выполниться до конца. Да это не очевидно, но в MSDN про это пишут.
Wyrd
12.04.2019 20:47Вообще, в целом я с Вами согласен (особенно надеюсь на статью код-как-английский-язык),
но! ИМХО Вы слишком категоричны, рассмотрите варианты типа threadPool.QueueWorkItem(() => {… }) — вполне возможно что внутри скобочек не
будет слова action => неискушённому взгляду не будет понятно что там ставят в очередь…JoshuaLight Автор
13.04.2019 00:02Вообще, в целом я с Вами согласен (особенно надеюсь на статью код-как-английский-язык)
Эта часть тоже уже вышла, но, судя по комментариям, она вас разочарует.
но! ИМХО Вы слишком категоричны
Того требует форма. Если бы на каждый пример я дописывал "вообще, бывают вот такие исключения, а вот в этих ситуациях всё-таки лучше так, а ещё случается...". Категоричность как раз была целью, поскольку её различаемость выше, чем у средних рассуждений (не в ущерб оным).
рассмотрите варианты типа threadPool.QueueWorkItem(() => {… }) — вполне возможно что внутри скобочек не
будет слова action => неискушённому взгляду не будет понятно что там ставят в очередь…И всё же, давайте представим сценарий. Разработчик, имеющий представление о потоках и о том, как они управляются в пуле (а это почти обязательное требование, чтобы им пользоваться), смотрит на запись
ThreadPool.Run( () => { ... } )
и такой думает: "Не, бред какой-то. Что тут происходит?". Потом встречаетThreadPool.QueueUserWorkItem
, и тотчас восклицает: "А, так это жUserWorkItem
! Теперь ясно!".
Боюсь, разработчики просто привыкают к
QueueUserWorkItem
, и для них это просто alias выражения "Вызвать метод на тредпуле".qw1
13.04.2019 01:02Боюсь, разработчики просто привыкают к QueueUserWorkItem, и для них это просто alias выражения «Вызвать метод на тредпуле»
Просто обманывать не надо. Если после завершения метода Run фактически выполнение не началось — это наименование врёт.JoshuaLight Автор
13.04.2019 10:30Просто обманывать не надо
Вы используете слово выполнить без контекста, тогда как в действительности это будет выполнить на тредпуле. Выполнение чего-то на тредпуле имеет другую семантику, чем просто выполнение чего-то. Почему бы тогда не писать
ThreadPool.FindFreeThreadAndExecuteUserWorkItem(action)
? А тоQueueUserWorkItem
не сообщает о том, что выполнение действия будет совершено только тогда, когда в тредпуле есть свободный поток.
Если после завершения метода Run фактически выполнение не началось — это наименование врёт.
Не имеет значения, началось ли оно фактически. Это инкапсулировано. Наименование врало бы, если бы разработчик ожидал от него мгновенного выполнения, а он этого не ожидает.
qw1
13.04.2019 11:18Почему бы тогда не писать ThreadPool.FindFreeThreadAndExecuteUserWorkItem(action)?
Потому что это неверно. Метод не ищет свободный поток. Он добавляет в очередь, и на этом его ответственность заканчивается. Если в имя метода добавить «Find», может показаться, что от наличия свободного потока зависит результат (если такого нет, метод вернёт false, например).JoshuaLight Автор
13.04.2019 11:56Потому что это неверно. Метод не ищет свободный поток.
Это потому что вы знаете, как устроен
ThreadPool
внутри. А метод, меж тем, не должен сообщать, что он делает внутри.
Мне, как клиенту
ThreadPool.QueueUserWorkItem
совершенно безразлично, будет там внутри очередь илиScheduler
. У меня есть действие, и я хочу выполнить его на потоке из пула. Поэтому: "Пул, подыщи-ка мне поток, и выполни эту задачу" — вот вам иFindFreeThreadAndExecuteUserWorkItem
получился.
Если в имя метода добавить «Find», может показаться, что от наличия свободного потока зависит результат (если такого нет, метод вернёт false, например).
Если прочитать только
Find
, но там есть ещёAndExecute
.
В общем, на мой взгляд, всё это несущественные споры. Основная претензия была не
Queue
(хотя и к нему есть), а кUserWorkItem
.
Как вам
ThreadPool.Queue
?qw1
13.04.2019 12:35Если прочитать только Find, но там есть ещё AndExecute.
«And» можно понять так, что действия выполняются последовательно. Если Find не успешен, Execute не выполняется.
Как вам ThreadPool.Queue?
Для данного класса — подходит, потому что ThreadPool с другими сущностями не работает. Но Microsoft можно понять: есть классы (например, Directory), которые работают с разными сущностями. Поэтому не просто Directory.Enumerate, а EnumerateFiles и EnumerateDirectories, и вообще, глагол+существительное как стандарт.
Тут консистентность наименований во всех классах важнее, чем экономия на одном простом классе.JoshuaLight Автор
13.04.2019 14:12Тут консистентность наименований во всех классах важнее, чем экономия на одном простом классе.
Мне так не кажется. Простой и читаемый код важнее, чем внутренняя согласованность библиотеки. Но речь, разумеется, не про Microsoft конкретно, а про общий подход.
Поэтому не просто Directory.Enumerate, а EnumerateFiles и EnumerateDirectories, и вообще, глагол+существительное как стандарт.
Зачем так, если можно хотя бы
Directories.Of(path)
иFiles.Of(path)
? Глагол не нужен совершенно. При этом лучше вообще использовать методы расширений, чтобы передать "субъектный" оттенок:path.AsDirectory().Files
иpath.AsDirectory().Directories
. Тогда и расширяемость выше.
netch80
13.04.2019 21:27Тогда уж лучше ThreadPool.Push().
IMHO, вызывает только желательные коннотации и в достаточном количестве.qw1
14.04.2019 01:10У push есть ассоциация со стеком, а он LIFO. Queue же FIFO.
netch80
14.04.2019 08:35> У push есть ассоциация со стеком, а он LIFO.
Не согласен. Ассоциация только у тех, для кого английский не родной и кто работал только со стеком.
В C++, например, у deque есть push_front, push_back, pop_front, pop_back, а у queue только push (в конец) и pop (из начала). То есть уже понимание не такое, как вам кажется.
У vector и многих прочих — только push_back.
Тот, кто их знает, уже не считает, что push всегда положить туда, откуда его же возьмёт pop.
А у нейтива вообще понимание, что push может быть какой угодно и где угодно, и понимать его надо по смыслу области применения.
Yoooriii
12.04.2019 02:13+1Вставлю свои 2 цента. Меня забавляет префикс Custom, например: CustomView, CustomController, CustomModel. Берем стандартный класс, и не особо думая добавляем Custom. Следующий наследник — это Custom Custom и так далее. Я встречал проекты, где чуть ли не половина классов называлась Custom. Реже встречается префикс My (MyClass, MyView). Этими обычно начинающие балуются, пока до Custom не дойдут (это уже считай уровень мидл). А вот у сеньеров с фантазией полный порядок. Например: BreadCrumps или BouncyCastle.
KaiOvas
12.04.2019 17:29Более того — начинающие разработчики часто, начитавшись каких-либо обучающих статей, лепят в свой код префиксы «Boo, Baz, Foo, Bar» которыми любят авторы статей именовать незначительные для повествования классы, методы и т.п. конструкции и в итоге потом жутко обижаются на то что их код заворачивается на код ревью с комментариями — переписать с вменяемыми названиями. Вот никогда не понимал, почему вставлять эти паразитные названия в демонстрационный код статьи? Ну если уж пишите статью то придумайте нормальные названия классов, интерфейсов, методов и потрудитесь не делать «Interface IBaz1» или «class FooBar». Это просто неуважение к читателю.
mayorovp
12.04.2019 18:27Напротив, неуважение к читателю — это когда его считают слишком глупым и неспособным воспринимать абстрактные примеры.
alex1t
Всё это уже ведь написано в хорошей книге «Чистый код» (Clean Code) Роберта Мартина
JoshuaLight Автор
Книга отличная!
Но этот цикл про несколько иную идею, хотя и пересекается с некоторыми разделами книги.
alex1t
Да, есть. Просто читаю статью и сразу вспоминаю книгу практически на каждом абзаце. Кто не читал книгу может полезно будет :)