Код живой и мёртвый. Часть первая. Объекты

habr.png

Код — это мысль. Появляется задача, и разработчик думает, как её решить, как выразить требования в функциях и классах, как сдружить их, как добиться строгости и корректности и как подмастить бизнес. Практики, методики, принципы, шаблоны и подходы — всё нужно учесть и всё нужно помнить.

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

Бороться предлагаю вот как: нужно представлять программы как текст на естественном языке и оценивать их соответственно. Как это и что получается — в статье.


Оглавление


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


Пролог

Мой опыт скромный (около четырёх лет), но чем больше работаю, тем сильнее понимаю: если программа нечитаемая, толку от неё нет. Давно уже известно и избито напоминать — код не только решает какую-то задачу сейчас, но ещё и потом: поддерживается, расширяется, правится. При этом он всегда: читается.

Ведь это текст.

Эстетика кода как текста — ключевая тема цикла. Эстетика тут — стёклышко, через которое мы смотрим на вещи и говорим, да, это хорошо, да, это красиво.

В вопросах красоты и понятности, слова имеют большое значение. Сказать: «В настоящий момент мои перцепции находятся в состоянии притупленности из-за высокого уровня этанола в крови» совсем не то же самое, что: «Я напился».

Нам повезло, программы почти полностью состоят из слов.

Скажем, нужно сделать «персонажа, у которого есть здоровье и мана, он ходит, атакует, использует заклинания», и сразу видно: есть объекты (персонаж, здоровье, мана, заклинание), действия (ходить, атаковать, использовать) и свойства (у персонажа есть здоровье, мана, скорость произнесения заклинаний) — всё это будут имена: классов, функций, методов, переменных, свойств и полей, словом, всего того, на что распадается язык программирования.

Но различать классы от структур, поля от свойств и методы от функций я не буду: персонаж как часть повествования не зависит от технических деталей (что его можно представить или ссылочным, или значимым типом). Существенно другое: что это персонаж и что назвали его 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 или не писать — вопрос спорный, несущественный, вкусовщина, да и какая вообще разница, если работает?

Но и из одноразовых стаканчиков можно пить!

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

Имя объекта — не только его лицо, но и бытие, самость. Оно определяет, будет он бесплотным или насыщенным, абстрактным или настоящим, сухим или оживлённым. Меняется имя — меняется содержание.

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

© Habrahabr.ru