Код живой и мёртвый. Часть первая. Объекты
Код — это мысль. Появляется задача, и разработчик думает, как её решить, как выразить требования в функциях и классах, как сдружить их, как добиться строгости и корректности и как подмастить бизнес. Практики, методики, принципы, шаблоны и подходы — всё нужно учесть и всё нужно помнить.
И вместе с этим мы видим повсеместную эпидемию менеджеров, хелперов, сервисов, контроллеров, селекторов, адаптеров, геттеров, сеттеров и другой нечисти: всё это мёртвый код. Он сковывает и загромождает.
Бороться предлагаю вот как: нужно представлять программы как текст на естественном языке и оценивать их соответственно. Как это и что получается — в статье.
Оглавление
- Объекты
- Действия и свойства
- Код как текст
Пролог
Мой опыт скромный (около четырёх лет), но чем больше работаю, тем сильнее понимаю: если программа нечитаемая, толку от неё нет. Давно уже известно и избито напоминать — код не только решает какую-то задачу сейчас, но ещё и потом: поддерживается, расширяется, правится. При этом он всегда: читается.
Ведь это текст.
Эстетика кода как текста — ключевая тема цикла. Эстетика тут — стёклышко, через которое мы смотрим на вещи и говорим, да, это хорошо, да, это красиво.
В вопросах красоты и понятности, слова имеют большое значение. Сказать: «В настоящий момент мои перцепции находятся в состоянии притупленности из-за высокого уровня этанола в крови» совсем не то же самое, что: «Я напился».
Нам повезло, программы почти полностью состоят из слов.
Скажем, нужно сделать «персонажа, у которого есть здоровье и мана, он ходит, атакует, использует заклинания», и сразу видно: есть объекты (персонаж, здоровье, мана, заклинание), действия (ходить, атаковать, использовать) и свойства (у персонажа есть здоровье, мана, скорость произнесения заклинаний) — всё это будут имена: классов, функций, методов, переменных, свойств и полей, словом, всего того, на что распадается язык программирования.
Но различать классы от структур, поля от свойств и методы от функций я не буду: персонаж как часть повествования не зависит от технических деталей (что его можно представить или ссылочным, или значимым типом). Существенно другое: что это персонаж и что назвали его Hero
(или Character
), а не HeroData
или HeroUtils
.
Теперь возьму то самое стёклышко эстетики и покажу, как сегодня пишется некоторый код и почему он далёк от совершенства.
Объекты
В C# (и не только) объекты — экземпляры классов, которые размещаются в куче, живут там некоторое время, а затем сборщик мусора их удаляет. Ещё это могут быть созданные структуры на стеке или ассоциативные массивы, или что-нибудь ещё. Для нас же они: имена классов, существительные.
Имена в коде, как и имена вообще, могут запутывать. Да и редко встретишь некрасивое название, но красивый объект. Особенно, если это Manager
.
Менеджер вместо объекта
UserService
, AccountManager
, DamageUtils
, MathHelper
, GraphicsManager
, GameManager
, VectorUtil
.
Тут главенствует не точность и осязаемость, а нечто смутное, уходящее куда-то в туман. Для таких имён многое позволительно.
Например, в какой-нибудь GameManager
можно добавлять что угодно, что относится к игре и игровой логике. Через полгода там наступит технологическая сингулярность.
Или, случается, нужно работать с фейсбуком. Почему бы не складывать весь код в одно место: FacebookManager
или FacebookService
? Звучит соблазнительно просто, но столь размытое намерение порождает столь же размытое решение. При этом мы знаем: в фейсбуке есть пользователи, друзья, сообщения, группы, музыка, интересы и т.д. Слов хватает!
Мало того, что слов хватает: мы ещё ими пользуемся. Только в обычной речи, а не среди программ.
И ведь не GitUtils
, а IRepository
, ICommit
, IBranch
; не ExcelHelper
, а ExcelDocument
, ExcelSheet
; не GoogleDocsService
, а GoogleDocs
.
Всякая предметная область наполнена объектами. «Предметы обозначились огромными пустотами», «Сердце бешено колотилось», «Дом стоял» — объекты действуют, чувствуются, их легко представить; они где-то тут, осязаемые и плотные.
Вместе с этим подчас видишь такое: в репозитории Microsoft/calculator
— CalculatorManager
с методами: SetPrimaryDisplay
, MaxDigitsReached
, SetParentDisplayText
, OnHistoryItemAdded
…
(Ещё, помню, как-то увидел UtilsManager
…)
Бывает и так: хочется расширить тип List<>
новым поведением, и рождаются ListUtils
или ListHelper
. В таком случае лучше и точнее использовать только методы расширения — ListExtensions
: они — часть понятия, а не свалка из процедур.
Одно из немногих исключений — OfficeManager
как должность.
В остальном же… Программы не должны компилироваться, если в них есть такие слова.
Действие вместо объекта
IProcessor
, ILoader
, ISelector
, IFilter
, IProvider
, ISetter
, ICreator
, IOpener
, IHandler
; IEnableable
, IInitializable
, IUpdatable
, ICloneable
, IDrawable
, ILoadable
, IOpenable
, ISettable
, IConvertible
.
Тут в основе сущности полагается процедура, а не понятие, и код снова теряет образность и читаемость, а слово привычное заменяется искусственным.
Куда живее звучит ISequence
, а не IEnumerable
; IBlueprint
, а не ICreator
; IButton
, а не IButtonPainter
; IPredicate
, а не IFilter
; IGate
, а не IOpeneable
; IToggle
, а не IEnableable
.
Хороший сюжет рассказывает о персонажах и их развитии, а не о том, как создатель создаёт, строитель строит, а рисователь рисует. Действие не может в полной мере представлять объект. ListSorter
это не SortedList
.
Возьмём, к примеру, DirectoryCleaner
— объект, занимающийся очисткой папок в файловой системе. Элегантно ли? Но мы никогда не говорим: «Попроси очистителя папок почистить D:/Test», всегда: «Почисти D:/Test», поэтому Directory
с методом Clean
смотрится естественнее и ближе.
Интереснее более живой случай: FileSystemWatcher
из .NET — наблюдатель за файловой системой, сообщающий об изменениях. Но зачем целый наблюдатель, если изменения сами могут сообщить о том, что они случились? Более того, они должны быть неразрывно связаны с файлом или папкой, поэтому их также следовало бы поместить в Directory
или File
(свойством Changes
с возможностью вызвать file.Changes.OnNext(action)
).
Такие отглагольные имена как будто оправдывает шаблон проектирования Strategy
, предписывающий «инкапсулировать семейство алгоритмов». Но если вместо «семейства алгоритмов» найти объект подлинный, существующий в повествовании, мы увидим, что стратегия — всего лишь обобщение.
Чтобы объяснить эти и многие другие ошибки, обратимся к философии.
Существование предшествует сущности
MethodInfo
, ItemData
, AttackOutput
, CreationStrategy
, StringBuilder
, SomethingWrapper
, LogBehaviour
.
Такие имена объединяет одно: их бытие основано на частностях.
Бывает, решить задачу быстро что-то мешает: чего-то нет или есть, но не то. Тогда думаешь: «Мне бы сейчас помогла штука, которая умеет делать X» — так мыслится существование. Затем для «делания» X пишется XImpl
— так появляется сущность.
Поэтому вместо IArrayItem
чаще встречается IIndexedItem
или IItemWithIndex
, или, скажем, в Reflection API вместо метода (Method
) мы видим только информацию о нём (MethodInfo
).
Более верный путь: столкнувшись с необходимостью существования, найти сущность, которая реализует и его, и, поскольку такова её природа, другие.
Например, захотели менять значение типа string
без создания промежуточных экземпляров — получилось прямолинейное решение в виде StringBuilder
, тогда как, на мой взгляд, уместнее — MutableString
.
Вспомним файловую систему: не нужен DirectoryRenamer
для переименования папок, поскольку, как только соглашаешься с наличием объекта Directory
, действие уже находится в нём, просто в коде ещё не отыскали соответствующий метод.
Если хочется описать способ взятия лока, то необязательно уточнять, что это ILockBehaviour
или ILockStrategy
, куда проще — ILock
(с методом Acquire
, возвращающим IDisposable
) или ICriticalSection
(с Enter
).
Сюда же — всяческие Data
, Info
, Output
, Input
, Args
, Params
(реже State
) — объекты, напрочь лишённые поведения, потому что рассматривались однобоко.
Где существование первично, там частное перемешано с общим, а имена объектов запутывают — приходится вчитываться в каждую строчку и разбираться, куда подевался персонаж и почему тут только его Data
.
Причудливая таксономия
CalculatorImpl
, AbstractHero
, ConcreteThing
, CharacterBase
.
Всё по тем же причинам, описанным выше, мы иногда видим объекты, для которых точно указывается место в иерархии. Вновь существование мчит впереди, вновь мы видим, как сиюминутную необходимость наспех выплеснули в код, не обдумав последствия.
Ведь разве бывает человек (Human
) — наследник базового человека (HumanBase
)? А как это, когда Item
наследует AbstractItem
?
Бывает, хотят показать, что есть не Character
, а некое «сырое» подобие — CharacterRaw
.
Impl
, Abstract
, Custom
, Base
, Concrete
, Internal
, Raw
— признак неустойчивости, расплывчатости архитектуры, который, как и ружье из первой сцены, позже обязательно выстрелит.
Повторения
Со вложенными типами бывает такое: RepositoryItem
— в Repository
, WindowState
— в Window
, HeroBuilder
— в Hero
.
Повторения разрежают смысл, усугубляют недостатки и только способствуют переусложнённости текста.
Избыточные детали
Для синхронизации потоков нередко используется ManualResetEvent
с таким API:
public class ManualResetEvent
{
// Все методы — часть `EventWaitHandle`.
void Set();
void Reset();
bool WaitOne();
}
Лично мне каждый раз приходится вспоминать, чем отличаются Set
от Reset
и что вообще такое «вручную сбрасывающееся событие» в контексте работы с потоками.
В таких случаях проще использовать далёкие от программирования (но близкие к повседневности) метафоры:
public class ThreadGate
{
void Open();
void Close();
bool WaitForOpen();
}
Тут уж точно ничего не перепутаешь!
Иногда доходит до смешного: уточняют, что предметы — не просто Items
, а обязательно ItemsList
или ItemsDictionary
!
Впрочем, если ItemsList
не смешно, то AbstractInterceptorDrivenBeanDefinitionDecorator
из Spring — вполне. Слова в этом имени — лоскуты, из которых сшито исполинское чудище. Хотя… Если это чудище, то что тогда — HasThisTypePatternTriedToSneakInSomeGenericOrParameterizedTypePatternMatchingStuffAnywhereVisitor
? Надеюсь, legacy.
Кроме имён классов и интерфейсов, часто встречаешь избыточность и в переменных или полях.
Например, поле типа IOrdersRepository
так и называют — _ordersRepository
. Но насколько важно сообщать о том, что заказы представлены репозиторием? Ведь куда проще — _orders
.
Ещё, бывает, в LINQ-запросах пишут полные имена аргументов лямбда-выражений, например, Player.Items.Where(item => item.IsWeapon)
, хотя что это предмет (item
) мы и без того понимаем, глядя на Player.Items
. Мне в таких случаях нравится использовать всегда один и тот же символ — x
: Player.Items.Where(x => x.IsWeapon)
(с продолжением в y
, z
если это функции внутри функций).
Итого
Признаюсь, с таким началом найти объективную правду будет непросто. Кто-то, например, скажет: писать Service
или не писать — вопрос спорный, несущественный, вкусовщина, да и какая вообще разница, если работает?
Но и из одноразовых стаканчиков можно пить!
Я убеждён: путь к содержанию лежит через форму, и если на мысль не смотрят, её как бы и нет. В тексте программы всё работает так же: стиль, атмосфера и ритм помогают выразиться не путано, а понятно и ёмко.
Имя объекта — не только его лицо, но и бытие, самость. Оно определяет, будет он бесплотным или насыщенным, абстрактным или настоящим, сухим или оживлённым. Меняется имя — меняется содержание.
В следующей статье, которая выйдет завтра, поговорим об этом самом содержании и о том, какое оно бывает.