В прошлый раз я писал о том, что имена объектов имеют большое значение, и что подбирать их нужно кропотливо и со вниманием к деталям. Плохое имя отпугивает и не даёт вникнуть в суть происходящего. Но что это за суть?
Сложно оценить героя, не поняв его "статы" и "абилки". Что он может и на что способен — вот следующий уровень сложности, на который нам придётся нырнуть. Мало с помощью точного имени отразить внутреннее святилище объекта, ещё следует убедиться, что это таки святилище, а не конюшни из геттеров.
Об этом — в статье.
Оглавление цикла
- Объекты
- Действия и свойства
- Код как текст
Действия
Персонаж атакует, защищается, уворачивается, стреляет из лука, использует заклинания, взмахивает клинком. Имя отражает объект, но сам объект — в движении, в реакции, в действиях. В противном случае мы бы говорили о таблицах в 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.
Текст от кода отличается ещё тем, что в первом случае вам заплатят, если дом стоит, утопая в лучах заходящего солнца; во втором — если дом стоит; но стоит помнить: стоять должен дом, а не палки из хелперов.
В первых двух статьях цикла я хотел показать, как важно работать не только над алгоритмом, но и словом; как названия определяют содержание называемого; как избыточный и переусложнённый код отгоняет читателя.
Вместе с этим, хорошие имена — только ноты. Чтобы заиграть, они должны стать написанными и воплотиться в музыке. Расскажу подробнее в следующей, заключительной статье.
