Привет, Хабр!
Как описать хороший код в трех словах? Исходя из опыта — это код, который [приятно поддерживать и эксплуатировать]. СОЛИД, SOLID, СОЛИД... Редко код ревью обходится без упоминания этих принципов, но для разработчика это не означает ничего хорошего. А что, если я скажу вам, что есть альтернатива? Недавно я наткнулся на одну англоязычную статью в блоге автора по имени Dan North. Он поднимает крайне интересные темы: написание хорошего кода, поддержка кодовой базы, порочные практики следования устаревшим трафаретам.
Думаю, для каждого программиста не секрет, что не всегда есть время и возможность подгонять свое решение под SOLID. Иногда проще оценивать свой код по шкале «хорошего кода» без трафаретов. Тут, конечно, прибегут некоторые люди, которые будут утверждать, что их проекты не содержат говнокода и легаси... Это все просто побег от реальности, нежелание принимать то, что любые проекты и их кодовые базы — это вечные «макароны». В общем, идеализация разработки, которая ни к чему, кроме душевных страданий, не ведет.
![Состояние отрицания у нашего пациента Состояние отрицания у нашего пациента](https://habrastorage.org/getpro/habr/upload_files/76a/ca2/630/76aca26303eef5e0d77c95cd1cd16088.jpeg)
Как показывает практика, именно оценка по гибким критериям дает самый удобный и переиспользуемый софт. Приведу пример из личного опыта.
У нас в Jmix есть «ассистент» по работе с фреймворком — Jmix Studio (кстати с недавного времени бесплатный). Так вот: мы разбивали студию по фичам; каждая фича оценивалась, насколько она востребована, как она написана по шкале «хорошего кода», насколько можно переиспользовать компоненты. Да, внутрянка нашей платформы соответствует SOLID... Но это же не все критерии оценки софта, а сам SOLID — это лишь внутренняя ретроспектива, которая оценивает одно свойство, а хотелось бы оценивать софт еще снаружи... <LongStory text={jmixStudioStory}/>. Из такого подхода (свойства востребованности и переиспользуемости) родился известный многим JPA Buddy, который, к тому же, стал частью IntelliJ IDEA. А изначально это был модуль Entity Designer внутри Jmix Studio для быстрого и удобного создания модели данных!
Вывод — гибкие методы оценки софта помогают создавать классные и переиспользуемые продукты, о которых вы сразу бы и не задумались. Short story long, перейдем к статье.
Предисловие к переводу
В процессе чтения, анализа и перевода оригинальной статьи я заметил, что автор выражает свои мысли крайне странными, но очень красивыми выражениями. При переводе я старался передать язык автора почти дословно. Поэтому не удивляйтесь, что текст звучит немного странно, такая уж манера у автора. Если не верите — продублирую ссылку на оригинальную статью.
CUPID — для программирования себе в радость
То, что началось как небольшой протест, как попытка пошатнуть SOLID, превратилось во что-то более конкретное и ощутимое. Если я не считаю принципы SOLID актуальными сегодня, то чем бы я их заменил? Может ли какой-либо набор принципов подходить для разработки программного обеспечения любого рода? Что мы подразумеваем под принципами?
Я уверен, что существуют свойства или характеристики софта, которые делают работу с ним приятной. Чем больше ваш код обладает этими качествами, тем больше удовольствия работать с ним, но все имеет свою цену, поэтому вам всегда следует учитывать контекст.
Вероятно, существует множество таких свойств, которые перекрываются друг другом и взаимосвязаны между собой, и существует множество способов их описать. Я выбрал пять свойств, которые покрывают большинство моих личных требований к коду. Здесь работает принцип убывающей отдачи: пять достаточно для создания удобного акронима, но не слишком много, чтобы не запомнить.
Я подробнее расскажу о каждом свойстве в будущих статьях, чтобы эта не стала слишком объемной, поэтому прошу прощения за то, что я не могу быть более всесторонним.
Пять принципов CUPID:
Композиционность: внутренние компоненты хорошо сочетаются друг с другом.
Философия Unix: делает одно дело, но делает его хорошо.
Предсказуемость: работает так, как вы ожидаете.
Идиоматичность: ощущается естественно.
Доменность: решение отражает предметную область в языке и структуре.
Преамбула: давным-давно...
Было ли у вас такое, что вы открывали чей-то код и сразу понимали, как в нем ориентироваться? Структура, нейминг, флоу — все ясно, знакомо так или иначе. Улыбка появляется на вашем лице. «Я справлюсь!» — думаете вы.
Мне повезло столкнуться с этим несколько раз за тридцатилетнюю карьеру и каждый раз это грело душу. Впервые это случилось в начале 1990-х — я это отчетливо помню — когда я открыл огромный проект на C, который выполнял сложную обработку изображений для цифровой печати. В чужом коде была ошибка, и мне предстояло ее найти и исправить. Я помню это чувство начинающего разработчика: смесь ужаса и страха выдать в себе дилетанта, которым я на самом деле и являлся.
Мой редактор кода — vim c утилитой ctags — позволил мне переходить к определениям функций из мест их вызова, и в течение нескольких минут я был погружен во вложенные вызовы, в кодовую базу из многих сотен исходных и заголовочных файлов, чувствуя уверенность в том, что я понимаю увиденное. Я быстро нашел виновника торжества, который оказался простой логической ошибкой, внес изменения, скомпилировал код и протестировал его. И все это без автоматизированного тестирования, только с использованием Makefiles. Впереди меня ожидал десятилетний опыт работы с TDD, а C в любом случае не имел таких инструментов.
Я запустил преобразование на нескольких образцах изображений, и они выглядели нормально. Я был уверен настолько, насколько это возможно, что я, во-первых, нашел и исправил ошибку и, во-вторых, не преподнес при этом никаких неприятных сюрпризов.
Приятное ПО
Бывает так, что код приносит удовольствие от работы с ним. Вы знаете, как найти то, над чем вам нужно работать. Вы знаете, как внести необходимые изменения. Код легко просматривать, легко понимать, легко анализировать. Вы уверены, что ваше изменение принесет желаемый результат без неожиданных побочных эффектов. Код направляет вас, приглашает оглядеться вокруг. Программист(ы), работавшие до вас, заботились о том, кто придет позже, возможно, потому что они понимали, что программистом, который придет позже, может быть один из них!
В своей фундаментальной книге «Рефакторинг» Мартин Фаулер говорит:
«Любой дурак может написать код, который поймет компьютер. Хорошие программисты пишут код, который понимают люди».
Я прочитал это в начале нулевых, и его слова перевернули мой мир программирования. А что, если хорошее программирование заключается в том, чтобы сделать код понятным для других людей? А что, если одним из этих людей буду я в будущем? Это звучало как что-то, к чему стоит стремиться.
Хотя «понятность» может быть благородной целью, но отнюдь не недостижимой! Примерно в то же время, когда Мартин писал о рефакторинге, пионер компьютерных технологий Ричард П. Габриэль (в книге Patterns of Software, стр. 7–16) описал идею обитаемости кода:
«Обитаемость — это характеристика исходного кода, позволяющая [людям] понимать его структуру и намерения, а также изменять его с комфортом и уверенностью. Обитаемость делает место пригодным для жизни, как дом».
Это кажется чем-то, к чему действительно стоит стремиться. Как было бы замечательно чувствовать себя уверенно и комфортно, изменяя код других людей? А если мы можем сделать код «обитаемым» — то что насчет того, чтобы сделать его приятным? Возможно ли, чтобы кодовая база наполняла вас радостью?
Если вы разрабатываете программный код, то навигация и манипуляция с кодом определяют ваш пользовательский опыт. Вы можете испытывать удивление, разочарование, страх, предвкушение, бессилие, надежду, радость — все это из-за решений, принятых предыдущими программистами.
Предположим, что кодовая база может быть приятной в использовании, является ли в таком случае каждая кодовая база уникальной, оказывающей неповторимое воздействие на вашу психику? Или мы можем выразить, что делает ее приятной, и предложить путь к росту удовольствия от кода, с которым мы работаем?
Свойства вместо принципов
Когда я начал формулировать ответ на пять принципов SOLID, я представил себе замену каждого из них на что-то, что я считал более полезным или актуальным. Вскоре я осознал, что сама идея принципов была проблематичной. Принципы похожи на правила: вы либо соответствуете им, либо нет. Это приводит к формированию «ограниченных множеств» сторонников и исполнителей правил, вместо «центрированных множеств» людей с общими ценностями.
Вместо этого я начал думать о свойствах: качествах или характеристиках кода, а не о правилах, которым нужно следовать. Свойства определяют цель или стандарт, к которому нужно стремиться. Ваш код либо ближе к стандарту, либо дальше от него, и всегда есть четкое направление движения. Вы можете использовать свойства как линзу или фильтр для оценки вашего кода, и вы можете решить, какие из них стоит улучшить в первую очередь. Поскольку свойства CUPID взаимосвязаны, вероятно, что любое изменение, направленное на улучшение одного свойства, окажет положительное воздействие на другие.
Свойства свойств
Итак, как нам выбирать свойства? Что делает свойство более или менее полезным? Я выделил три «свойства свойств», которыми, по моему мнению, должны обладать свойства CUPID. Они должны быть практичными, человечными (human) и многоуровневыми.
Чтобы быть практичными, свойства должны быть:
легко формулируемыми: чтобы вы могли описать каждое из них в нескольких предложениях, предложить конкретные примеры и контрпримеры.
легко оцениваемыми: чтобы вы могли использовать их как линзу для просмотра и обсуждения кода, а также легко решить, насколько код демонстрирует каждое свойство.
легко принимаемыми: чтобы вы могли начать с малого и постепенно развивать код в соответствии с любым из измерений CUPID. Нет необходимости «вкладываться всем», и нет «неудачи», так же, как и никогда не бывает «готово». Код всегда может быть улучшен.
Чтобы быть человечными, свойства должны рассматриваться с точки зрения людей, а не кода. CUPID говорит о том, каково работать с кодом, но не дает абстрактное описание самого кода. Например, философия Unix «делать одну вещь и делать ее хорошо» может звучать как принцип единственной ответственности, но первое касается того, как вы используете код, а второе — внутренней структуры самого кода.
Чтобы быть многоуровневыми, свойства должны предлагать рекомендации для начинающих — что является следствием легкости формулировки, — и нюансы для более опытных специалистов, которые хотят более глубоко исследовать природу программного обеспечения. Каждое из свойств CUPID понятно уже по названию и краткому описанию, но каждое включает в себя множество уровней, измерений, подходов. Мы можем описать «стандарт» для каждого свойства, но существует множество путей, чтобы добраться до него!
Композиционность
Программное обеспечение, которое легко использовать, используется снова и снова. Существуют характеристики, которые делают код более или менее композиционным, но они ни в коем случае не обязательны и недостаточны для каких-либо гарантий. В каждом случае мы можем найти контрпримеры с обеих сторон, поэтому следует рассматривать их как полезные эвристики. Больше не всегда значит лучше; это всегда компромисс.
Маленькая площадь поверхности
Когда используется код с узко ориентированным API, вы тратите меньше времени на изучение, сокращается вероятность ошибок и конфликтов с другим кодом. Однако это имеет убывающую ценность; если ваши API слишком узкие, вам приходится использовать их группами, и знание «правильной комбинации» для общих случаев использования становится неявным знанием, которое может быть преградой для входа. Получить правильную гранулярность API намного сложнее, чем кажется. Существует оптимальный баланс связности между фрагментированным и раздутым API.
Отражение намерений
Код, отражающий намерения, легко изучать и оценивать. Я легко могу найти компонент и так же быстро решить, подходит ли он мне или нет. Одна из моделей, которая мне нравится, — это иметь двухминутное, десятиминутное руководство и глубокое погружение, как в открытых проектах, например, знаменитом XStream. Это позволяет мне инвестировать постепенно и сразу переключаться, когда я понимаю, что это не для меня.
Не раз я начинал писать класс, давая ему имя, отражающее намерения, только чтобы среда разработки предложила мне импорт с тем же именем. Обычно оказывалось, что кто-то другой пришел к тому же самому выводу, и я случайно нашел их код, потому что мы выбрали похожие имена. Это не было просто случайностью; мы находились в одном домене, что делало более вероятным то, что мы выберем похожие имена. Это более вероятно, когда у вас есть доменный код.
Минимальные зависимости
Код с минимальными зависимостями дает вам меньше поводов для беспокойства и снижает вероятность несовместимости версий или библиотек. Я написал свой первый проект с открытым исходным кодом XJB на Java, используя практически вездесущую библиотеку логирования log4j. Коллега указал на то, что это создает зависимость не только от log4j как библиотеки, но и от конкретной версии. Я даже не думал об этом; почему кому-то должно быть не все равно на такую невинную вещь, как библиотека логирования? Поэтому мы удалили зависимости и даже выделили целый другой проект, который занимался интересными вещами с динамическими прокси в Java, который сам по себе имел минимальные зависимости.
Философия Unix
Мы с Unix примерно одного возраста, оба появились в 1969 году, и с тех пор он стал самой распространенной операционной системой на планете. В 1990-х годах каждый серьезный производитель компьютерного оборудования имел свою версию Unix, пока ключевые варианты с открытым исходным кодом, Linux и FreeBSD, не стали повсеместными. Сегодня Unix управляет почти всеми бизнес-серверами как в облаке, так и на локальных серверах в виде Linux; он работает во встроенных системах и сетевых устройствах; он лежит в основе операционных систем macOS и Android; он даже предоставляется как необязательная подсистема для Microsoft Windows!
Простая, консистентная модель
Так как же нишевая операционная система, начавшая свой путь в лаборатории исследования телекоммуникаций, становится копией хобби-проекта студента и в итоге превращается в самую крупную операционную систему в мире? Безусловно, существуют коммерческие и юридические причины ее успеха в эпоху, когда производители операционных систем были известны своими судебными исками друг против друга наряду с их технологиями, но ее долговечная техническая привлекательность заключается в ее простой и последовательной философии дизайна.
Философия Unix предписывает создавать компоненты, которые хорошо работают вместе (описано в свойстве Композиции выше), и которые делают одно дело, но делают его хорошо. Например, команда ls выводит сведения о файлах и директориях, но она ничего не знает о файлах или директориях! Есть системная команда под названием stat, которая предоставляет эту информацию; ls является всего лишь инструментом для представления этой информации в текстовом виде.
Точно так же команда cat выводит (конкатенирует) содержимое одного или нескольких файлов, grep выбирает текст, соответствующий заданному шаблону, sed заменяет текстовые шаблоны и так далее. Командная строка Unix обладает мощной концепцией «пайпов», которая позволяет подключать вывод одной команды в качестве входных данных для следующей, создавая конвейер для выбора, преобразования, фильтрации, сортировки и так далее. Вы можете писать сложные программы для обработки текста и данных, сочетая в себе несколько хорошо спроектированных команд, каждая из которых делает одну вещь и делает ее хорошо.
Одна цель вместо единой ответственности
На первый взгляд это похоже на принцип единой ответственности (Single Responsibility Principle), и для некоторых трактовок SRP существует некоторое перекрытие. Однако «делать одно дело, но делать его хорошо» — это взгляд снаружи внутрь; это свойство иметь конкретную, четко определенную и всеобъемлющую цель. SRP — это взгляд изнутри наружу: речь идет об организации кода.
SRP, словами Роберта С. Мартина, который придумал этот термин, означает, что код «должен иметь одну, и только одну, причину для изменения». Пример в статье Википедии — это модуль, который создает отчет, в котором вы должны рассматривать содержимое и формат отчета как отдельные задачи, которые должны находиться в отдельных классах, или даже отдельных модулях. Как я говорил ранее, на моем опыте, это создает искусственные разделы, и наиболее обычный случай — это когда содержимое и формат данных изменяются вместе; например, новое поле или изменение источника некоторых данных, что влияет как на их содержимое, так и на способ их отображения.
Еще один распространенный сценарий — это «UI компонент», где SRP требует, чтобы вы разделили отрисовку и бизнес-логику компонента. Для разработчика наличие этих элементов в разных местах приводит к административной рутине по соединению одинаковых полей вместе. Больший риск заключается в том, что это может быть преждевременной оптимизацией, препятствующей более естественному разделению задач по мере роста кодовой базы и появления компонентов, которые «делают одно дело, но делают его хорошо» и которые лучше соответствуют доменной модели предметной области. По мере роста любой кодовой базы наступит время разделить ее на разумные подкомпоненты, но свойства Композиции и Структуры, основанной на домене, будут лучшим индикатором того, когда и как делать эти структурные изменения.
Предсказуемость
Код должен делать то, что от него ожидают, последовательно и надежно, без неприятных сюрпризов. Проверить это должно быть не только возможно, но и легко. В этом смысле предсказуемость — это обобщение тестируемости.
Предсказуемый код должен вести себя ожидаемо, быть детерминированным и наблюдаемым.
Ведет себя ожидаемо
Первое из четырех правил простого дизайна Кента Бека — код «проходит все тесты». Это должно быть верно даже тогда, когда тестов нет! Предполагаемое поведение предсказуемого кода должно быть очевидным из его структуры и именования. Если нет автоматизированных тестов для проверки этого, написать их должно быть легко. Майкл Фезерс называет такие тесты «характеризующими».
«Когда система выходит в продакшн, в каком-то смысле она становится своей собственной спецификацией.» — Майкл Фезерс.
Я заметил не редкий кейс, что некоторые люди относятся к разработке через тестирование (TDD) как к религии, а не как к инструменту. Я когда-то работал над сложным приложением для алгоритмической торговли, в котором было около 7% покрытия тестами. Эти тесты были распределены не равномерно! Большая часть кода вообще не имела автоматизированных тестов, а некоторая — сумасшедшее количество сложных тестов, проверяющих мелкие ошибки и пограничные случаи. Я был уверен во внесении изменений в большую часть кодовой базы [без сайд-эффектов], потому что каждый из компонентов делал одно дело, и его поведение было простым и предсказуемым, так что изменение обычно было очевидным.
Детерминированность
Программное обеспечение должно делать одно и то же каждый раз. Даже код, предназначенный для недетерминированной работы — например, генератор случайных чисел или динамический расчет — будет иметь операционные или функциональные границы, которые вы можете определить. Вы должны иметь возможность предсказать границы памяти, сети, хранения или обработки, временные границы и ожидания по другим зависимостям.
Детерминированность — это обширная тема. В контексте предсказуемости детерминированный код должен быть устойчивым, надежным и отказоустойчивым.
Устойчивость — это широта или полнота ситуаций, которые мы охватываем. Ограничения и крайние случаи должны быть очевидными.
Надежность — это действие в соответствии с ожиданиями в ситуациях, которые мы охватываем. Мы должны получать одни и те же результаты каждый раз.
Отказоустойчивость — определяет, насколько хорошо мы справляемся с ситуациями, которые мы не охватываем; среди них, например, неожиданные изменения во входных данных или операционной среде.
Наблюдаемость
Код должен быть наблюдаемым в контексте Теории управления: мы можем вывести его внутреннее состояние по его выходным данным. Это возможно только тогда, когда мы проектируем это заранее. Как только несколько компонентов взаимодействуют, в особенности асинхронно, возникнет непредвиденное поведение и нелинейные последствия.
«Инструментирование» кода с самого начала означает, что мы можем получить ценные данные для понимания его характеристик во время выполнения. Я описываю четырехступенчатую модель с двумя дополнительными ступенями так:
Инструментирование — это когда ваше программное обеспечение говорит, что оно делает.
Телеметрия — это предоставление этой информации, будь то по запросу — когда что-то спрашивает, — или по инициативе — отправка сообщений; «измерение на расстоянии».
Мониторинг — это прием данных от инструментирования и их визуализация.
Сигнализация — это реакция на отслеживаемые данные или на определенные закономерности в данных.
Дополнительно:
Прогнозирование — это использование этих данных для предвидения событий до их наступления.
Адаптация — это изменение системы динамически, либо для предотвращения, либо для восстановления после предполагаемого изменения.
Большинство программного обеспечения даже не проходит первый этап. Существуют инструменты, которые перехватывают или изменяют работающие системы для добавления уровня прозрачности, но они никогда не будут так же хороши, как преднамеренное инструментирование, заложенное в приложение.
Идиоматичность
У каждого есть свой стиль написания кода. Будь то использование пробелов вместо табуляции, размер отступа, соглашения о наименовании переменных, расположение фигурных скобок или круглых скобок, организация кода в исходном файле или множество других возможностей. К этому мы можем добавить выбор библиотек, инструментария, пути к рабочей версии, даже стиль комментариев в системе контроля версий или детализацию коммитов. (Вы же используете систему контроля версий, не так ли?)
Это может добавить значительную избыточную когнитивную нагрузку при работе с незнакомым кодом. Помимо понимания предметной области и пространства решений, вам нужно интерпретировать, что имел в виду кто-то другой, и были ли их решения обдуманными и контекстуальными или произвольными и привычными.
Лучшее качество программиста — это эмпатия; эмпатия к вашим пользователям; эмпатия к технической поддержке; эмпатия к будущим разработчикам; любой из которых может быть будущим вами. Писать «код, который понятен людям», означает писать код для кого-то другого. Вот что значит идиоматичный код.
В этом контексте ваша целевая аудитория:
знакома с языком, его библиотеками, инструментарием и экосистемой
опытный программист, понимающий специфику разработки программного обеспечения
стремится выполнить работу в срок и с надлежащим качеством!
Идиомы языка
Код должен соответствовать идиомам языка программирования. Некоторые языки имеют строгие представления о том, как должен выглядеть код, что упрощает оценку идиоматичности вашего кода. Другие менее придирчивы, что перекладывает на вас обязанность «выбрать стиль» и затем придерживаться его. Go и Python — два примера многозначащих языков.
Программисты на Python используют термин «pythonic» для описания идиоматичного кода. Существует замечательная пасхалочка, которая появляется, если вы импортируете this в Python REPL или запускаете python -m this из оболочки. Это выводит список программных афоризмов, названных «Zen of Python», который включает в себя эту строку, отражающую дух идиоматичного кода: «Должен быть один — и желательно только один — очевидный способ сделать это».
Язык Go поставляется с форматировщиком кода под названием gofmt, который делает весь исходный код одинаковым. Это мгновенно устраняет любые разногласия по поводу отступов, расположения фигурных скобок или других синтаксических особенностей. Это означает, что любые примеры кода, которые вы видите в документации, библиотеке или учебниках, выглядят последовательно. Существует даже документ под названием Effective Go, который демонстрирует идиоматичный Go, выходящий за рамки определения языка.
На другом конце спектра находятся такие языки как Scala, Ruby, JavaScript и почтенный Perl. Эти языки намеренно имеют много парадигм; Perl придумал акроним TIMTOWTDI — «There Is More Than One Way To Do It» («Существует более чем один способ сделать это»), произносится как «Тим Тоади». На большинстве из них можно писать функциональный, процедурный или объектно-ориентированный код, что создает пологую кривую обучения из любого языка, который вы знаете. Для такой простой задачи, как обработка последовательности значений, большинство этих языков позволяют вам:
использовать итератор
использовать цикл for с индексом
использовать условный цикл while
использовать функциональный конвейер с коллектором («map-reduce»)
написать хвостовую рекурсивную функцию
Это означает, что в любом коде, который не является тривиальным, вы, скорее всего, найдете примеры каждого из этих подходов, часто сочетающиеся друг с другом. Опять же, это все увеличивает когнитивную нагрузку, влияя на вашу способность думать о текущей проблеме, увеличивая неопределенность и уменьшая удовольствие от работы.
Идиомы кода встречаются на всех уровнях детализации: именование функций, типов, параметров, модулей; расположение кода; структура модулей; выбор инструментов; выбор зависимостей; управление зависимостями и так далее.
Независимо от того, где находится ваш технологический стек на шкале догматичности, написанный вами код будет более эмпатичным и приносить больше радости, если вы уделите время на изучение идиом языка, его экосистемы, сообщества и предпочтительного стиля.
Ваша кривая обучения новой технологии, скорее всего, будет короче, чем срок службы любого кода, написанного на ней, поэтому важно сопротивляться желанию писать код, который хорошо читается для вас прямо сейчас, потому что этот человек не будет рядом надолго! Единственный способ быть уверенным, что вы пишете идиоматичный код, — это уделить время на изучение идиом.
Локальные идиомы
Когда в языке нет консенсуса относительно идиоматического стиля или существует несколько альтернатив, выбор «хорошего» стиля зависит от вас и вашей команды, а также от введения ограничений и рекомендаций для обеспечения согласованности. Эти ограничения могут быть такими простыми, как общие правила форматирования кода в вашей среде разработки, инструменты «полицейского сборки», которые анализируют (тут автор говорит про linter-ы) и критикуют код, а также соглашения относительно стандартного набора инструментов.
Записи об архитектурных решениях (Architecture Decision Records, ADR) являются отличным способом документировать ваши выборы в стиле и идиомах. Эти решения не менее «значимы» с технической точки зрения, чем любое другое обсуждение архитектуры.
Доменность
Мы пишем программное обеспечение, чтобы удовлетворить потребность. Эта потребность может быть конкретной и ситуационной или общей и широкомасштабной. Независимо от ее цели, код должен передавать то, что он делает, на языке предметной области, чтобы минимизировать когнитивное расстояние между тем, что вы пишете, и тем, что он делает. Это больше, чем просто «использование правильных слов».
Доменный язык
Языки программирования и их библиотеки полны структур из компьютерных наук, таких как хеш-мэпы, связные списки, древовидные множества, подключения к базам данных и так далее. Они имеют базовые типы, состоящие из целых чисел, символов, булевых значений. Вы можете объявить фамилию человека как строковую переменную (string), что, возможно, и будет способом ее хранения, но определение типа Фамилия будет более выразительным. Он даже может иметь операции, свойства или ограничения, связанные с фамилией. Многие мелкие ошибки в банковском программном обеспечении связаны с представлением денежных сумм в виде чисел с плавающей точкой; опытные программисты финансового программного обеспечения определят тип данных «Деньги с полями Валюта и Сумма», которая сама по себе является составным типом.
Хорошее именование типов и операций не только предотвращает ошибки, но и делает простой навигацию по пространству решений в коде. Я сделал это своим вкладом в «97 вещей, которые каждый программист должен знать», как «Код на языке домена».
Одним из критериев успешности в кодировании, ориентированном на домен, является то, что случайный наблюдатель не может определить, обсуждают ли люди код или домен. Я столкнулся с этим однажды в системе электронной торговли, где финансовый аналитик обсуждал сложную логику ценообразования с двумя программистами. Я думал, что они обсуждают правила ценообразования, но они указывали на экран, полный кода, а аналитик проводил программистов через алгоритм ценообразования, который был построчно таким, как читался код! Единственной когнитивной дистанцией между проблемной областью и кодовым решением были некоторые синтаксические знаки препинания!
Доменная структура
Использование языка, основанного на домене, важно, но также важно и то, как вы структурируете свой код. Многие фреймворки предлагают «скелет проекта» с распределением по директориям и файлам-заглушкам, разработанным для того, чтобы вы могли быстро начать работу. Это навязывает вашему коду априорную структуру, не имеющую ничего общего с решаемой вами проблемой.
Вместо этого распределение кода — это имена директорий, отношения дочерних и соседних папок, группировка и именование связанных файлов — должны отражать предметную область настолько точно, насколько это возможно.
Фреймворк Ruby on Rails сделал этот подход популярным в начале 2000-х годов, встроив его в свои инструменты, и широкое распространение Rails означало, что многие последующие фреймворки скопировали эту идею. CUPID нейтрален к языкам и фреймворкам, но Rails служит полезным примером для понимания разницы между структурой, основанной на домене, и структурой, основанной на фреймворке.
Ниже приведена часть структуры директорий сгенерированного скелета приложения Rails, сфокусированная на директории (приложения), в которой разработчик проведет большую часть своего времени. Полный скелет на момент написания этого текста включает около 50 директорий, содержащих 60 файлов.
app
├── assets
│ ├── config
│ ├── images
│ └── stylesheets
├── channels
│ └── application_cable
├── controllers
│ └── concerns
├── helpers
├── javascript
│ └── controllers
├── jobs
├── mailers
├── models
│ └── concerns
└── views
└── layouts
Представьте, что это будет приложение для управления больницей с разделом для записи пациентов. Эта структура предполагает, что нам понадобится как минимум:
модель, которая соответствует базе данных;
представление, которое отображает запись пациента на экране;
контроллер, который служит посредником между представлениями и моделями.
Затем есть область для помощников (helpers), assets-ов и нескольких других концепций фреймворка, таких как модели или контроллеры, почтовые рассылки, задания, каналы и, возможно, контроллеры JavaScript-а, который будет сопровождать ваш контроллер Ruby. Каждый из этих артефактов находится в отдельной директории, хотя семантически они тесно интегрированы.
Скорее всего, любое нетривиальное изменение в управлении записями пациентов потребует кода, разбросанного по всей кодовой базе. Принцип единой ответственности в SOLID говорит о том, что код представления должен быть отделен от кода контроллера, и фреймворки вроде Rails интерпретируют это так, что они должны находиться в совершенно разных местах. Это увеличивает когнитивную нагрузку, снижает связность и увеличивает усилия по внесению изменений в продукт. Как я говорил ранее, эта идеологическая ограниченность может усложнить работу и сделать кодовую базу менее приятной.
Нам все еще нужны артефакты наподобие моделей, представлений и контроллеров, независимо от того, как мы организуем код, но группировка их по типу не должна формировать первичную структуру. Вместо этого, на верхнем уровне кодовой базы должны отображаться основные случаи использования системы управления больницей; возможно, patient_history, appointments, staffing и compliance.
Применение доменного подхода к структуре кода облегчает понимание того, для чего он предназначен, и упрощает навигацию к тому месту, где вы должны находиться для решения любой задачи, более сложной, чем «сделать эту кнопку светло-голубой».
Доменные границы
Когда мы структурируем код так, как мы хотим, называем его так, как нам удобно — границы модулей становятся доменными, и расширение становится простым. Все, что нам нужно для создания компоненты в виде единого артефакта, находится вместе, поэтому мы можем согласовать доменные границы с границами расширения и создания связи между бизнес-компонентой и сервисом. Независимо от того, упаковываете ли вы ваши продукты или услуги в один монолит, множество мелких микросервисов или где-то посередине, это согласование упрощает эксплуатацию вашего кода и снижает вероятность того, что вы что-то забудете или включите артефакты из другой среды или другой подсистемы.
Это не ограничивает нас одним плоским верхним уровнем структуры кода. Домены могут содержать поддомены; компоненты могут содержать подкомпоненты; расширение может происходить на любом уровне детализации, который имеет смысл для вашего профиля изменений и рисков. Согласование границ кода с доменными границами делает все эти варианты более понятными и легкими в управлении.
Заключительные мысли
Я считаю, что код, обладающий бОльшим количеством этих свойств — композиционность, философия Unix, предсказуемость, идиоматичность или доменность — более приятен в работе, чем код, который ими не обладает. Хотя я ценю каждое из этих качеств независимо, я нахожу, что они взаимно усиливают друг друга.
Код, который одновременно композиционен и всесторонен — делающий одно дело хорошо — напоминает надежного друга. Идиоматичный код кажется знакомым, даже если вы никогда раньше его не видели. Предсказуемый код дает вам дополнительные ресурсы для концентрации на неожиданностях в других местах. Доменный код минимизирует когнитивное расстояние от потребности к решению. Движение кода в сторону «центра» любого из этих свойств оставляет его лучше, чем вы его нашли.
Поскольку CUPID является обратным акронимом, у меня было несколько кандидатов на каждую букву. Я выбрал эти пять, потому что они каким-то образом кажутся «фундаментальными»; мы можем вывести все другие свойства-кандидаты из этих. В будущих статьях будут рассмотрены некоторые из свойств, которые не попали в финальный список, и будет рассмотрено, как они являются естественными последствиями написания программного обеспечения с использованием CUPID.
Мне интересно услышать о приключениях людей с CUPID. Я уже слышал о командах, использующих эти свойства для оценки своего кода и для разработки стратегий по очистке устаревших кодовых баз, и я с нетерпением жду отчетов об опыте и кейс-стади. Между тем я хочу более глубоко изучить CUPID, рассмотреть каждое из свойств по очереди, чтобы увидеть, что еще скрывалось из виду.
Я рекомендую всем, кто занимается разработкой программного обеспечения, не только программистам, прочитать это короткое эссе. Это глубокий и красивый текст.
В 1970-х годах Пол Г. Хиберт, антрополог и христианский миссиолог (наблюдатель за миссионерами), использовал математическое понятие ограниченных и центрированных множеств для сравнения «ограниченных» сообществ, которые определяют себя правилами, кто внутри, а кто снаружи, с «центрированными» сообществами, которые определяют себя набором основных ценностей, от которых люди могут быть ближе или дальше, но никогда не «снаружи».
Определение единственной ответственности заключается в том, что код должен иметь «одну и только одну причину для изменения», при этом идея состоит в том, что вы должны, например, разделить пользовательский интерфейс и бизнес-логику. Не только этот принцип тривиален для опровержения — на том основании, что даже одна строка кода может потребовать изменений по причинам безопасности, соответствия стандартам, внешним или внутренним зависимостям, операционным характеристикам и так далее, но я воспринимаю это как произвольное ограничение, которое часто является преждевременным разделением с отрицательными последствиями.
Кроме того, в дизайне операционной системы Unix есть элегантная простота: все является либо файлом, либо текстом, либо не текстом; мы строим целые программы, обрабатывая текст через серию преобразований.
Ruby может быть исключением здесь, поскольку определенно существует «эстетика Ruby», и разные люди писали о «идиоматическом Ruby», но это все равно индивидуальные предпочтения в стиле программирования, а не что-то врожденное для сообщества.
Архитектурные решения были впервые предложены Майклом Нигардом в 2011 году и с тех пор постоянно развиваются.
Существует целый ряд других дискуссий о том, сколько шаблонов и сгенерированных заготовок фреймворк должен навязывать разработчику для «чистого» проекта, что выходит за рамки этой статьи.
Вывод
Итак, что выбираете вы: строгие рамки SOLID или свобода и креативность с CUPID? Лично для меня эта крайне близкая к сердцу тема. Мы в команде Jmix решили миксовать оценку кода CUPID и применять SOLID, когда это удобнее. Дайте знать в комментариях, какой подход, по вашему мнению, является наиболее приятным и применимым в вашей практике разработки.
![](https://habrastorage.org/getpro/habr/upload_files/cd6/594/9a5/cd65949a5969e60204190116d88b6bda.png)