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


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


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


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


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


  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/calculatorCalculatorManager с методами: 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) или ICriticalSectionEnter).


Сюда же — всяческие 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 или не писать — вопрос спорный, несущественный, вкусовщина, да и какая вообще разница, если работает?


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


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


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


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




К сожалению, не доступен сервер mySQL