По дате публикации, данная статья является завершением, а на самом деле предисловием к циклу статей написанных по теме чистого кода. В статьях отразил свой взгляд на создание гибкого и читаемого программного кода, где акцент сделан на примерах.

Текущая же статья посвящена общим принципам написания кода, который легко читать, понимать и поддерживать (KISS если так больше нравится). Сразу хочу предупредить, что многие положение отраженные в этой статье могут вызвать сомнения в их корректности, но это личное мнение автора, опирающееся на опыт.

Чистый код не для всех. Признает это читатель или нет, но сообщество разработчиков не однородно. Нас нельзя сравнивать даже в начале карьеры. Между выпускником месячных курсов на Python, и выпускником университета обладающего теорией и знанием нескольких языков программирования большая разница. Да и должностное разделение никто не отменял.

Результат опроса разработчиков

На изображение приведена выборка из результатов опроса разработчиков от StackOverflow за 2023 год (2023 Developer Survey).

Понимание концепции чистого кода приходит с опытом. Именно понимание, а не знание. Можно заучить наизусть и повторять на собеседовании все принципы, но не понимать как их применять.

Чистый код является прерогативой опытных разработчиков (Developer Experience). Именно такие специалисты организовывают работу над проектом, таким образом, чтобы правила построения чистого кода выполнялись как бы сами собой и были доступны для выполнения даже новичками.

Чистый код без предварительно проектирования бесполезен. Проектирование позволяет организовать и разбить весь программный продукт на модули еще до начала процесса написания кода. Плохо организованный, читайте спроектированный проект, всегда ведет к финансовым потерям, а именно из-за финансовой выгоды мы все и работаем.

На этом моменте со мной часто начинают спорить, приводя множество методик управления проектами: Waterfall, Agile, Critical Path Method, Critical Chain Project Management, Scrum, Scrumban, PRINCE2, Extreme Programming. Это только те, с которыми я знаком, как говорят, «смог потрогать руками».

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

Ну а теперь пояснения, как метод управления проектом связан с проектированием и причем здесь чистый код.

Надеюсь, только мне не повезло. Ни разу не пришлось участвовать, да и не слышал, о проектах, в которых бы с начала до конца участвовал экономист. Этот график обобщенный взгляд, составленный из поздних оценок затрат, где по оси X отложено время в процентах, а по Y, процент затрат по бюджету.

График проекта

Из графика понятно, что идеальный проект, это прямая линия. К 100% все работы выполнены и все уложилось в бюджет.

В реальности ни один план не соблюдается на сто процентов, и все доработки поверх оговоренных выполняются за счет разработчиков. Здесь тоже можно возразить. Указать на то как правильно составлять договор, или считать сумму проекта, но суть не в этом. Разработчики могут повлиять на «прогиб» этого графика только организацией работ. Распределением всех задач, равномерно по всей протяженности проекта.

Вот в этот момент принято говорить про Agile, Scrum и др., но в реальности все зависит от правильного проектирования. Весь функционал проекта необходимо правильно, подчеркну, правильно разделить на составляющие. Еще до написания первой строчки кода, необходимо понимать, из каких частей он состоит и количество этих частей должно соответствовать принципу KISS, то есть каждый модуль должен быть прост в понимании.

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

И на первом этапе проекта (точки на графике) хорошо себя показывают жесткие приемы управления (например Waterfall). Желательно его соблюдать даже на этапе согласования проекта с заказчиком. Но это как повезет.

Второй этап, создание минимально жизнеспособного продукта (Minimum Viable Product - MVP), здесь лучше ослабить жесткий контроль и управлять более гибко. Обычно помогает если модули разрабатываются последовательно, формируя работоспособный каркас (например Critical Path Method), поддающийся комплексному и нагрузочному тестированию. Выявляя проблемы на ранней стадии.

К последним двум этапам архитектурные ошибки уже должны быть выявлены и становится важно, правильно ли команда проектирования разделила проект на модули. Опытная команда, перед началом разработки может составить близкий к реальному список задач, который поддается управлению через гибкий подход (например Agile).

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

Настало время пояснить, как управление проектами связано с чистым кодом. Чистый код, и соответствие принципу KISS (по моему, это одно и тоже), во многом, является следствием правильной организации работы над проектом. Качественно спроектированный и управляемый проект приводит к предсказуемости изменений. Не приходится на ходу переписывать целые блоки кода, так как весь проект разделен на небольшие модули, которые взаимодействуют между собой через SOLID, Design Pattern и др.

Чистый код использует паттерны проектирования естественным образом. Собственно, данное утверждение и не требует дополнительного разъяснения, но стоит уточнить. В русском языке, заимствованный термин, «паттерны проектирования» воспринимаются как единая смысловая конструкция. Как например понятие «окно» буквально, это проем в стене, но мы подразумеваем конструкцию из стекла и рамы.

Так и с термином «паттерны проектирования». Design patterns переводится как шаблоны проектирования, то есть шаблоны которые используются при проектировании. Следовательно и использовать их необходимо при проектировании.

Разработчик без достаточного опыта программирования, не может правильно выполнить проектирование, а следственно и применить шаблоны. Требовать этого с простых программистов не следует, на это должна быть команда проектирования. Как видите, по опросу разработчиков от StackOverflow, и получают они в два раза больше.

Когда с терминами разобрались, приведу пример задания от группы проектирования из реального проекта. Примерно в таком виде оно попало на доску заданий (Task board).

/* Модуль хеширования пароля.
* Алгоритм хеширования - ГОСТ 34.11-2018
* Соль - "qwery"
* Количество циклов хеш + соль — 5
* Параметры: pass – сторока символов
* Возврат — хэш в виде строки
*/

namespace ModulePasswordHash {
	class IPasswordHash
	{
	public:
		virtual std::string get(std::string pass) = 0;
	};
}

Из очевидного, здесь применен шаблон Фасад (Facade), для отделения модуля от бизнес логики. Выполнена инверсия зависимости, для тех же целей. Комментарии, после реализации блока уйдут в документацию через систему документирования.

Из не очевидного, программисту не надо ничего знать о том как этот модуль будет использоваться, его задача проста и понятна. Меньше неоднозначности, меньше ошибок. В случае если возникнет необходимость в изменениях, проблема легко локализуется, и даже если внести изменения трудно. Можно легко заменить весь модуль.

Тестирование такого функционала просто и эффективно, но об этом дальше.

Чистый код не требует усилий на тестирование. Тестирование как обеспечение качества (QA - quality assurance), сформулировано в отдельную дисциплину относительно недавно поэтому, обросло мифами и противоречивыми утверждениями.

Первое, из таких необоснованных утверждений предполагает, что тесты должен писать тот кто пишет основной код. Это противоречит всему опыту технологического развития, и здравому смыслу. Что если качество строительства будут утверждать сами строители. Что если свежесть блюд будут утверждать сами сотрудники точек питания.

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

Второе утверждение, которое не выдерживает проверки практикой говорит о том, что тестами необходимо покрывать сто процентов кода. Излишнее покрытие тестами тормозит последующее развитие программного обеспечения. Практика показывает, что при модификации или исправлении ошибочных решений, часто необходимо выполнять структурные преобразования в коде, что влечет переработку тестовой базы.

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

Все эти действия не только занимают много производительного времени, но и не имеют четких критериев оценки. Как определить, где находится та граница за которой дальнейшие модификации не эффективны и тесты необходимо переписать.

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

Вот здесь и оказывает свое влияние экономика. Как правило, на завершающих этапах разработки корректируются требования заказчика и в полную силу включаются механизмы цикличности (Agile). То же происходит в момент выпуска обновлений. В обоих случаях, сумма выделенная на проект уже практически освоена. Увеличение сроков, а значит и бюджета должны иметь веские обоснование. В которые восстановление целостности тестовой базы не входит.

Получается, что восстановление тестовой базы откладывается, а после просто не проводится. Чем дальше от момента первых тестов, тем дороже становится их поддержка. Автор не раз наблюдал, как команда разработчика выдвигала вполне обоснованные основания для откладывания переработки тестовой базы, и менеджмент соглашался с ними. Заведомо ухудшая стабильность системы. Исполнение требований заказчика выше по приоритету.

Ну и очевидным решение этой проблемы является изначальное сокращение области покрытия тестами. Основной единицей разработки является модуль, и в разработке тестов в том числе. Минимальной единицей тестирования рассматривается «фасад» модуля. Внутри этого функционала программист вправе принимать решении о написании дополнительных тестов самостоятельно.

Чистый код, простая разработка через тестирование (Test-Driven Development TDD). Такое утверждение основывается на простой идее, если до начала разработки вы можете получить подробно описанный интерфейс модуля, то почему перед написанием самого модуля не написать тест проверяющий его работу.

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

При следующем цикле разработки, команда проектировки создает новые требования. Специалист по тестированию проверяет актуальность тестов, выдаются задания на исправление тестов, а позже и основного кода на доску (Task board). При этом программист полностью свободен в принятии решения. Он вправе полностью удалить функционал модуля и написать его заново, или изменить только небольшую его часть.

Чистый код решает проблемы производительности. Главным аргументом противников применения практики чистого кода является производительность. Использование наследования, виртуальных функций, мелких классов, множества модулей и др. замедляет работу логики. Это утверждение верно, но практика показывает, что большинство проблем с производительностью возникает из-за человеческого фактора. Ошибки при построении бизнес-логики, неправильно выбранный каркас (framework), некорректно используемые алгоритмы влияют на снижение производительности значительно больше чем виртуальные таблицы. Модульное построение приложения, возможность быстрой модификации и простота построения интегральных и нагрузочных тестов значительно снижают количество ошибок влияющих на производительность.

Чистый код плохо сочетается с готовыми фраймворк (framework). Как и в случае с паттернами, это проблема не самих фреймворк как таковых, а некорректного их использования. Фреймворк (анг. framework — «каркас, структура»), это просто инструмент, который позволяет решить типовые задачи и использовать их необходимо именно как инструмент.

К сожалению довольно часто каркас (framework) становится из инструмента, каким-то фетишем, почти религиозного направления. Приходится сталкиваться с ситуациями, когда разработчики, сначала выбирают каркас (framework), а потом начинают вникать в бизнес-логику приложения.

Плохой практикой является и использование каркасов (framework) не по назначению. Это когда берется многофункциональный каркас и из него используется менее 50% функционала. Лучше использовать несколько маленьких и не таких модных, чем объять все стразу.

Следующим тонким моментом является планирование обновления каркасов (framework). На этапах проектирования необходимо сразу принять решение, будет ли каркас вашего приложения обновляться, вслед за выпуском его новых версий. Если нет, то это необходимо указать в документации, пояснить заказчику и тогда можно напрямую использовать его функционал. Жестко интегрируя с написанном кодом.

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

Чистый код отменяет рефакторинг. Рефа́кторинг (англ. refactoring), или перепроектирование кода, переработка кода, равносильное преобразование алгоритмов — процесс изменения внутренней структуры программы, не затрагивающий её внешнего поведения и имеющий целью облегчить понимание её работы.

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

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

Заключение. К сожалению мы живем не в идеальном мире, и каждый программист не может писать чистый код по своей инициативе. Простой, понятный, надежный и легко масштабируемый код пишут коллективы.

Поэтому, решение проблемы чистого кода видится в правильной организации группы разработчиков. Опытные, имеющие соответствующие навыки программисты, формируют проект, его простоту понятность и надежность. Не имеющие такого опыта занимаются конкретной реализацией и учатся, в том числе и на ошибках более опытных.

Автор: Юрий Е. - 2024 г.