Качество кода — Критический фактор успеха любого программного проекта. Некорректные наименования сущностей, избыточная сложность методов, противоречивые комментарии и нарушение структурных принципов ведут к существенным проблемам: снижению скорости разработки, росту количества ошибок и сложности поддержки. Роберт Мартин (известный как «Дядя Боб»), один из авторов Agile-манифеста, в своей фундаментальной работе «Чистый код» систематизировал принципы написания эффективного, поддерживаемого кода.
Книга «Чистый код» это детально проработанная методология, основанная на десятилетиях опыта разработки коммерческих систем. Она отвечает на ключевые вопросы:
-
Как создать код, который понятен коллегам через месяцы после написания?
-
Какие паттерны предотвращают накопление технического долга?
-
Как проектировать компоненты для лёгкого тестирования и модификации?
Все примеры в этой статье будут на языке программирования C#. Каждый раздел сопровождается примерами рефакторинга «плохого» кода в «чистый» согласно стандартам Мартина.
Что такое чистый код?
Мартин начинает с фундамента: чистый код — это код, написанный с заботой о читателе (часто будущем вас или коллеге). Этот код:
-
Прямолинеен: Делает ровно то, что ожидается.
-
Лаконичен: Ничего лишнего. Одна операция — одна функция, одна ответственность — один класс.
-
Выразителен: Имена и структура и без комментариев объясняет, что делается и почему.
-
Поддерживаем: Легко изменять и расширять без страха сломать что-то ещё.
-
Проверен: Покрыт осмысленными юнит-тестами.
Основа всего — имена
Правильные названия — 90% чистого кода. Вот основные правила:
-
Выразительность: Имя должно отвечать на все главные вопросы: Что это? Зачем нужно? Как используется? Избегайте общих слов (
data,info,manager), если они не полностью говорят о смысле.-
Плохо:
int d; // elapsed time in days -
Хорошо:
int elapsedTimeInDays; -
Плохо:
List list1; -
Хорошо:
List activeCustomers;
-
-
Избегайте дезинформации:
Не используйтеaccountListдля переменной типаAccount[](это не список). ЛучшеaccountsилиaccountGroup.
Не используйте похожие имена:XYZControllerForEfficientHandlingOfStringsиXYZControllerForEfficientStorageOfStrings. -
Делайте осмысленные различия:
-
Плохо:
Product,ProductInfo,ProductData(что отличает их?). -
Плохо:
a1, a2, ... aN. -
Хорошо:
sourceCustomer,targetCustomer;originalMessage,encryptedMessage.
-
-
Используйте произносимые имена:
genymdhms(generate date, year, month, day, hour, minute, second) — кошмар. ЛучшеgenerationTimestamp. -
Используйте поисковые имена: Однобуквенные имена (
i,j,kв коротких циклах — исключение) и числовые константы (15) сложно найти в коде.-
Плохо:
if (employee.Flags & 15) ... -
Хорошо:
const int IsFullTimeFlag = 0x01; const int IsOnProbationFlag = 0x02;
-
-
Классы / Типы: Имена существительных или существительных с уточнением (
Customer,AddressParser,AccountService). ИзбегайтеManager,Processor,Data,Info. -
Методы: Имена глаголов или глагольных фраз (
Save(), Delete(), ParseConfiguration(), CalculateTotal()). Геттеры/сеттеры —GetName(),SetName(). -
Булевы переменные / методы: Используйте префиксы
is,has,can,shouldдля ясности:isActive,hasLicense,canExecute,shouldValidate.
Методы и функции
Маленькие, хорошо организованные методы это главное чистого кода!
-
МАЛЕНЬКИЕ! И ещё раз МАЛЕНЬКИЕ!
-
Идеал: Не длиннее 20 строк. Часто 3-4 строки.
-
Правило экрана: Функция должна полностью помещаться на одном экране без прокрутки.
-
-
Делайте одно дело (The Single Responsibility Principle — SRP для функций):
-
Функция должна делать ТОЛЬКО то, что явно следует из ее имени. Если вы можете выделить другую операцию с осмысленным именем — вынесите ее в отдельную функцию.
-
Уровни абстракции: Операции внутри функции должны быть на ОДНОМ уровне абстракции. Не смешивайте высокоуровневую логику (
ProcessOrder()) с низкоуровневыми деталями (ParseOrderLineItemString()).
-
-
Структура кода (Сверху-Вниз — Stepdown Rule):
-
Код должен читаться как повествование, сверху вниз.
-
На верхнем уровне высокоуровневые функции (шаги алгоритма).
-
Каждый следующий уровень — более детальные функции, реализующие шаги верхнего уровня.
-
-
Аргументы Функций (Параметры):
-
Идеал: 0 (niladic) > 1 (monadic) > 2 (dyadic). 3 (triadic) — Избегать! > 3 — Требует исключительного обоснования.
-
Флаги как аргументы — ЗЛО! (
Process(bool isAdmin)) — Это явный признак, что функция делает два разных дела! Разбейте на две:ProcessAdmin(),ProcessUser(). -
Output-параметры (out, ref) — Еще большее зло! Они запутывают и нарушают поток чтения. Возвращайте кортежи или маленькие объекты-результаты.
-
Объекты-аргументы: Если нужно много параметров, логически связанных, объедините их в класс/структуру.
-
Плохо:
public void CreateReservation(DateTime start, DateTime end, int roomId, string customerName, bool hasPremium) -
Хорошо:
public class ReservationRequest{
public DateTime Start { get; set; }
public DateTime End { get; set; }
public int RoomId { get; set; }
public CustomerInfo Customer { get; set; } // CustomerInfo содержит Name и HasPremium
}
public void CreateReservation(ReservationRequest request)
-
-
-
Глаголы и ключевые слова:
-
Один аргумент: Имя функции и аргумента должны образовывать естественную пару глагол/существительное:
WriteField(name),AssertExpectedEqualsActual(expected, actual). -
Два аргумента: Порядок должен следовать общепринятому:
Point p = new Point(x, y);.
-
-
Исключения вместо кодов ошибок:
-
Плохо: Возврат кодов ошибок (
int result = Save(); if (result == ERROR) ...) ведет к загромождению кода проверками сразу после вызова. -
Хорошо: Бросайте исключения (
try { Save(); } catch (SaveException ex) ...). Обработка ошибок отделена от основной логики. -
Try[Операция]Паттерн: Для ситуаций, где ошибка это часть ожидаемого потока (например, проверка пароля), используйте паттерн
TryParse:if (int.TryParse(input, out int value)) { ... } // Успех
else { ... } // Ошибка, но не исключение
-
-
DRY (Don’t Repeat Yourself): Безжалостно устраняйте дублирование кода, выделяя общую функциональность в методы.
Пример рефакторинга функции (C#):
Грязный метод:

Чистые методы:

Комментарии
Лучший комментарий — отсутствующий комментарий. Хороший код объясняет себя сам через имена и структуру. Комментарии часто лгут (код меняется, а комментарии нет) и загромождают. Но есть исключения:
-
Законные комментарии:
-
Правовые: Лицензии, авторские права.
-
Поясняющие намерение (WHY): Почему код сделан так, а не иначе? Особенно если причина неочевидна (баг в библиотеке, специфичное требование).

-
Предупреждения о последствиях:
// Внимание: Этот метод запускает долгую операцию (до 5 мин). Не вызывать из UI-потока! -
TODO: Краткие заметки о том, что нужно доделать / поправить позже. Обязательно указывайте контекст / причину.

-
Усиление (Amplification): Подчеркнуть важность чего-то неочевидного.
// Не изменять порядок инициализации: компонент Y зависит от X! -
Javadoc в Public API: Для публичных методов/классов библиотек — объяснение, что делает, параметры, возвращаемое значение, исключения.
-
-
Недопустимые комментарии:
-
Закомментированный код: Удаляйте его! Системы контроля версий хранят историю.
-
Избыточные:

-
История изменений:

-
Скобочные / позиционные:
// Конец метода CalculateTotal ------------------------ -
Комментарии-извинения:
// Извините за этот баг... -
Неясные ссылки:
// См. замечание в мануале— Какой мануал? Какое замечание?
-
Форматирование
Единый стиль форматирования критически важен для читаемости.
-
Вертикальное форматирование (Длина файла, пустые строки, группировка):
-
Длина файла: Стремитесь к маленьким файлам (200-500 строк). Большие классы — признак нарушения SRP.
-
Пропуски (Пустые строки): Разделяйте логические блоки внутри файла (группы переменных, методы, логические секции внутри большого метода).
-
Вертикальная Близость: Связанные концепции должны быть рядом. Переменная объявляется как можно ближе к месту использования. Вызываемые методы — ниже вызывающих (Правило Stepdown).
-
-
Горизонтальное форматирование (Длина строки, отступы, пробелы):
-
Длина строки: 120 символов — разумный максимум. Избегайте горизонтальной прокрутки.
-
Отступы (Indentation): Обязательны! Используйте отступы (обычно 4 пробела) для вложенных блоков (внутри классов, методов, циклов, условий).
-
Пробелы: Используйте для улучшения читаемости:
-
Вокруг операторов (
=,+,-,*,/,%,==,!=,>,<,>=,<=,&&,||):int sum = a + b; -
После запятых в списках аргументов/параметров:
void Print(string name, int age) -
После ключевых слов
(if, for, while, switch, catch): if (condition) -
Между скобками и содержимым (
кроме пустых):list.Add( item ); -> list.Add(item); -
Не ставьте пробелы между именем методы и открывающейся скобкой:
Save()(неSave ()). А также внутри скобок индексатора:array[0](неarray[ 0 ])
-
-
Выравнивание: Избегайте выравнивания переменных по типу или значению. Оно создаёт ложный акцент и сложно поддерживается.
-
Плохо:

-
Хорошо:

-
-
Объекты и структуры данных
-
Абстракция и сокрытие данных: Классы должны скрывать свои данные и предоставлять абстрактные интерфейсы для работы с ними. Не создавайте просто структуру данных с публичными полями (в C# для этого есть
struct, но и их поля лучше делать приватными).-
Плохо (публичные поля):

-
Хорошо (инкапсуляция):

-
-
Закон Деметера (Principle of Least Knowledge): Метод объекта
MобъектаOдолжен вызывать только методы:-
Самого объекта
O(this). -
Объектов, переданных в
Mв качестве параметров. -
Объектов, созданных внутри
M. -
Компонентов объекта
O.-
Не
objectA.GetObjectB().DoSomething();— Это «поездка по цепочке». Нарушает инкапсуляцию, делает код хрупким. -
Решение: Делегирование. Пусть
objectAпредоставит метод, выполняющий нужное действие, скрыв свою зависимость отObjectB:
Плохо:
Хорошо:

-
-
Обработка ошибок
Ошибки — часть жизни ПО. Обрабатывайте их чисто:
-
Используйте исключения, а не коды ошибок.
-
Сначала пишите Try-Catch-Finally: Обрабатывайте код, который может выбросить исключение, блоком
try. Обрабатывайте конкретные исключения вcatch. Освобождайте ресурсы вfinally. -
Не возвращайте null! Это источник
NullReferenceException(для C#, аналогично в других языках). Возвращайте пустые коллекции (Enumerable.Empty(), new List()), используйтеNullable<T>для значимых типов, применяйте паттерн Null Object. -
Не передавайте null! Это смещает ответственность за проверку на вызывающего. Бросайте
ArgumentNullExceptionв начале метода, еслиnullнедопустим.
Использование стороннего кода
Работа с библиотеками/API требует аккуратности:
-
Изолируйте границы: Не позволяйте стороннему коду «расползаться» по всей вашей кодовой базе. Оберните его в адаптер (
AdapterилиFacadeпаттерн).
-
Изучайте через тесты: Напишите небольшие юнит-тесты для сторонней библиотеки перед интеграцией. Это поможет понять её поведение и защитит от неожиданных изменений при обновлении.
Юнит-Тесты. TDD и чистота тестов
Без тестов нет чистого кода. Тесты обеспечивают безопасность рефакторинга.
-
TDD (Test-Driven Development) цикл:
-
Красный: Напишите маленький тест для новой функциональности. Он должен упасть (так как функциональности ещё нет).
-
Зелёный: Напишите минимальный код, чтобы тест прошёл. Можно схитрить.
-
Рефакторинг (Синий): Улучшите структуру кода (как продакшн, так и тест), удалите дублирование, примените принципы чистого кода. Тесты должны оставаться зелёными.
-
-
Правила чистых тестов (F.I.R.S.T.):
-
F (Fast): Тесты должны выполняться мгновенно (секунды, а не минуты). Медленные тесты не запускают часто.
-
I (Independent): Тесты не должны зависеть друг от друга. Порядок выполнения не важен. Сброс состояния перед каждым тестом.
-
R (Repeatable): Результат теста одинаков в любой среде (dev, CI, продакшн) и при любом количестве запусков. Нет зависимостей от сети, времени, случайных чисел.
-
S (Self-Validating): Тест должен выдать бинарный результат: УСПЕХ или ПРОВАЛ. Нет ручной проверки логов.
-
T (Timely): Пишите тесты либо до написания кода (в подходе TDD), либо немедленно после написания рабочего кода. Никогда не откладывайте написание тестов на потом!
-
-
Качество тест-кода: Так же важно, как и продакшн код. Применяйте все правила чистого кода: выразительные имена, маленькие функции, минимальные утверждения (Asserts) на тест, отсутствие дублирования (используйте
SetUp/TearDownили фабричные методы аккуратно), ясность (Build-Operate-Check паттерн).
Классы
-
Принцип единственной ответственности (Single Responsibility Principle — SRP): У класса должна быть только одна причина для изменения. Если класс делает слишком много, его нужно разбить.
Признаки нарушения SRP: Много несвязанных методов/полей, частые изменения в разных местах класса по разным причинам, большой размер. -
Инкапсуляция: Скрывайте данные и детали реализации! Делайте поля приватными. Предоставляйте доступ через методы / свойства. Ослабляйте уровень доступа только при явной необходимости.
-
Композиция прежде наследования (Composition over Inheritance): Наследование создаёт сильную связь между классами. Часто композиция (включение одного класса в другой как поле) + интерфейсы дают больше гибкости.
-
Плохо:

-
Хорошо:

-
Системы
Чистота на уровне архитектуры.
-
Разделение забот (Separation of concerns): Система должна быть разделена на слабо связанные модули (например, по слоям: UI, бизнес-логика, доступ к данным). Каждый модуль решает свою задачу.
-
Чистая архитектура (Clean Architecture/Onion/Hexagonal): Основная идея — зависимости направлены внутрь, к ядру. Бизнес-правила (самые стабильные) в центре. Инфраструктура (БД, UI, фреймворки — часто меняющиеся) на периферии. Ядро не зависит от деталей реализации периферии. Достигается через интерфейсы и DI.
-
Внедрение зависимостей (Dependency Injection — DI, дополнительно Zenject): Классы не создают свои зависимости, а получают их извне (через конструктор, свойства, методы). Это ослабляет связность, упрощает тестирование и делает код гибче.

-
Масштабирование: Система должна начинаться с простой, чистой архитектуры. Не добавляйте сложности «на вырост»! Рефакторите и расширяйте архитектуру по мере реальной необходимости.
Пример рефакторинга
Мартин детально разбирает эволюцию небольшой программы (генератор простых чисел) от грязной первой версии до чистого кода. Этапы:
-
Начало: Рабочая, но ужасно написанная версия.
-
Написание тестов: Покрытие функциональности тестами для безопасности изменений.
-
Рефакторинг волнами:
-
Разбиение огромного метода на маленькие.
-
Улучшение имён переменных и функций.
-
Устранение дублирования.
-
Упрощение логики.
-
Улучшение алгоритма.
-
-
Результаты: Код становится в разы короче, понятнее, эффективнее и легче для изменения.
Словарик терминов
-
Чистый код (Clean code) — Код, который легко читать, понимать и изменять. Обладает свойствами: прямолинейность, лаконичность, выразительность, поддерживаемость, проверенность.
-
Выразительность (Expressiveness) — Свойство кода, при котором имена переменных / методов / классов и структура однозначно передают их назначение и логику без комментариев.
-
DRY (Don’t Repeat Yourself) — Принцип разработки «Не повторяйся». Любое знание или логика должны иметь единственное представление в системе.
-
SRP (Single Responsibility Principle) — Для функций/классов: Объект должен иметь только одну причину для изменения (выполнять одну обязанность).
-
Инкапсуляция (Encapsulation) — Сокрытие внутреннего состояния объекта и деталей реализации. Доступ к данным только через публичные методы / свойства.
-
Закон Деметры (Law of Demetr / Principle of Least Knowledge) — Метод объекта
Aдолжен взаимодействовать только с:-
Самим
A -
Параметрами, переданными в метод
-
Объектами, созданными внутри метода
-
Непосредственными компонентами
A. Запрещает цепочки вызовов:objA.GetB().DoC()
-
-
Исключение (Exception) — Механизм обработки ошибок, прерывающий нормальный поток выполнения и передающий управление обработчику (
catch). Альтернатива кодам ошибок. -
Null-объект (Null Object Pattern) — Паттерн, подразумевающий возврат специального объекта с нейтральным поведением вместо
null(например, пустая коллекция). -
Адаптер (Adapter Pattern) — Паттерн для преобразования интерфейса класса в другой интерфейс, ожидаемый клиентом. Используется для изоляции стороннего кода.
-
Фасад (Facade Pattern) — Паттерн, предоставляющий простой интерфейс к сложной подсистеме. Скрывает её детали реализации.
-
Юнит-тест (Unit Test) — Автоматизированный тест, проверяющий корректность работы небольшой изолированной части кода (метода, класса).
-
TDD (Test-Driven Development) — Методология разработки:
🔴 Red → Написать падающий тест
🟢 Green → Написать минимальный код для прохождения теста
🔵 Refactor → Улучшить код, сохраняя зелёный статус. -
F.I.R.S.T. — Принципы чистых тестов:
-
Fast — Быстрые
-
Independent — Независимые
-
Repeatable — Повторяемые
-
Self-Validating — Самопроверяющиеся
-
Timely — Своевременные.
-
-
Arrange-Act-Assert (AAA) — Паттерн структуры юнит-теста:
-
Arrange: Подготовка данных и зависимостей
-
Act: Вызов тестируемого метода
-
Assert: Проверка результата.
-
-
Внедрение зависимостей (DI, Dependency Injection) — Передача зависимостей объекта извне (через конструктор / свойства) вместо их создания внутри. Уменьшает связанность.
-
Чистая архитектура (Clean Architecture) — Архитектурный подход, где:
-
Бизнес-логика (ядро) не зависит от деталей (БД, UI, фреймворков)
-
Зависимости направлены к центру системы
-
-
Разделение забот (SoC, Separation of Concerns) — Принцип разделения программы на независимые модули, каждый из которых решает отдельную задачу (например: UI, бизнес-логика, БД).
-
Композиция (Composition) — Построение функциональности за счёт включения одних объектов в другие (предпочтительнее наследования).
-
Рефакторинг (Refactoring) — Изменение структуры кода без изменения его поведения. Цель — улучшение читаемости и упрощение поддержки.
-
Непрерывный рефакторинг (Continuous Refactoring) — Практика постоянного улучшения кода в процессе разработки.
-
Дурной запах кода (Code Smell) — Симптом проблемы в коде (например: длинный метод, дублирование, «божественный класс»).
-
Уровень абстракции (Level of Abstraction) — Степень детализации операций. Код внутри функции должен оперировать на одном уровне (например, только бизнес-логика или только технические детали).
-
Правило пошагового спуска (Stepdown Rule) — Код должен читаться сверху вниз: высокоуровневая логика -> детали реализации. Каждый уровень раскрывает подробности предыдущего.
-
Искажение (Misinformation) — Использование имён, вводящих в заблуждение (например:
accountListдля массива). -
Обучающие тесты (Learning Tests) — Тесты, написанные для изучения поведения сторонней библиотеки перед её использованием в проекте.
-
Границы (Boundaries) — Места интеграции с внешними системами (библиотеки, API, устаревший код). Требуют изоляции.
-
Модуль (Module) — Логически связанная группа функций / классов (например: компонент, слой, библиотека).
-
Связанность (Coupling) — Степень зависимости между модулями. Низкая связанность — признак хорошего дизайна.
-
Зацепление (Cohesion) — Мера связанности обязанностей внутри модуля. Высокое зацепление — все элементы модуля работают на единую цель.
-
Искажение абстракции (Abstraction Leakage) — Ситуация, когда детали реализации просачиваются через абстракцию, нарушая инкапсуляцию.
Если понравилась статья — рекомендую подписаться на телеграм‑канал NetIntel. Там вы сможете найти множество полезных материалов по IT и разработке!
ссылка на оригинал статьи https://habr.com/ru/articles/922424/
Добавить комментарий