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