Код живой и мёртвый. Часть вторая. Действия и свойства

в 9:21, , рубрики: .net, architecture design, C#, design patterns, fluent, fluent interface, ood, refactoring, solid, ооп, Программирование, Проектирование и рефакторинг, Совершенный код

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

Сложно оценить героя, не поняв его "статы" и "абилки". Что он может и на что способен — вот следующий уровень сложности, на который нам придётся нырнуть. Мало с помощью точного имени отразить внутреннее святилище объекта, ещё следует убедиться, что это таки святилище, а не конюшни из геттеров.

Об этом — в статье.

Оглавление цикла

  1. Объекты
  2. Действия и свойства
  3. Код как текст

Действия

Персонаж атакует, защищается, уворачивается, стреляет из лука, использует заклинания, взмахивает клинком. Имя отражает объект, но сам объект — в движении, в реакции, в действиях. В противном случае мы бы говорили о таблицах в Excel.

В C# действия — методы и функции. А для нас: глаголы, атомы словесного движения. Глаголы двигают время, из-за них объекты существуют и взаимодействуют. Где есть изменение — там должен быть глагол.

Сеттеры

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

Например, есть IPullRequest со свойством Status, которое может быть Approved, Declined или Merged. Можно писать pullRequest.Status = Status.Declined, но это то же самое, что говорить “Установи пул-реквесту отменённый статус”, — императивно. Куда сильнее — pullRequest.Decline() и, соответственно, pullRequest.Approve(), pullRequest.Merge().

Активный глагол предпочтительнее сеттера, но не все глаголы такие.

Пассивный залог

PerformPurchase, DoDelete, MakeCall.

Как в HeroManager важное существительное заслоняется бессмысленным Manager, так и в PerformMigrationPerform. Ведь живее — просто Migrate!

Активные глаголы освежают текст: не “нанёс удар”, а “ударил”; не “сделал замах”, а “замахнулся”; не “принял решение”, а “решил”. Так и в коде: PerformApplicationApply; DoDeleteDelete; PerformPurchasePurchase, 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.

Текст от кода отличается ещё тем, что в первом случае вам заплатят, если дом стоит, утопая в лучах заходящего солнца; во втором — если дом стоит; но стоит помнить: стоять должен дом, а не палки из хелперов.

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

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

Автор: JoshuaLight

Источник

* - обязательные к заполнению поля