- Регистрация
- 23 Август 2023
- Сообщения
- 2 815
- Лучшие ответы
- 0
- Реакции
- 0
- Баллы
- 51
Offline
Качество кода - Критический фактор успеха любого программного проекта. Некорректные наименования сущностей, избыточная сложность методов, противоречивые комментарии и нарушение структурных принципов ведут к существенным проблемам: снижению скорости разработки, росту количества ошибок и сложности поддержки. Роберт Мартин (известный как "Дядя Боб"), один из авторов Agile-манифеста, в своей фундаментальной работе "Чистый код" систематизировал принципы написания эффективного, поддерживаемого кода.
Книга "Чистый код" это детально проработанная методология, основанная на десятилетиях опыта разработки коммерческих систем. Она отвечает на ключевые вопросы:
Все примеры в этой статье будут на языке программирования C#. Каждый раздел сопровождается примерами рефакторинга "плохого" кода в "чистый" согласно стандартам Мартина.
Что такое чистый код?
Мартин начинает с фундамента: чистый код - это код, написанный с заботой о читателе (часто будущем вас или коллеге). Этот код:
Правильные названия - 90% чистого кода. Вот основные правила:
Маленькие, хорошо организованные методы это главное чистого кода!
Пример рефакторинга функции (C#):
Грязный метод:
Чистые методы:
Комментарии
Лучший комментарий - отсутствующий комментарий. Хороший код объясняет себя сам через имена и структуру. Комментарии часто лгут (код меняется, а комментарии нет) и загромождают. Но есть исключения:
Единый стиль форматирования критически важен для читаемости.
Ошибки - часть жизни ПО. Обрабатывайте их чисто:
Работа с библиотеками/API требует аккуратности:
Без тестов нет чистого кода. Тесты обеспечивают безопасность рефакторинга.
Чистота на уровне архитектуры.
Мартин детально разбирает эволюцию небольшой программы (генератор простых чисел) от грязной первой версии до чистого кода. Этапы:
Если понравилась статья - рекомендую подписаться на телеграм‑канал NetIntel. Там вы сможете найти множество полезных материалов по IT и разработке!
Книга "Чистый код" это детально проработанная методология, основанная на десятилетиях опыта разработки коммерческих систем. Она отвечает на ключевые вопросы:
Как создать код, который понятен коллегам через месяцы после написания?
Какие паттерны предотвращают накопление технического долга?
Как проектировать компоненты для лёгкого тестирования и модификации?
Все примеры в этой статье будут на языке программирования 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 (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 и разработке!