Как стать автором
Обновить

Прочитал «Чистый код», чтобы вам не пришлось

Уровень сложностиПростой
Время на прочтение12 мин
Количество просмотров13K

Качество кода - Критический фактор успеха любого программного проекта. Некорректные наименования сущностей, избыточная сложность методов, противоречивые комментарии и нарушение структурных принципов ведут к существенным проблемам: снижению скорости разработки, росту количества ошибок и сложности поддержки. Роберт Мартин (известный как "Дядя Боб"), один из авторов Agile-манифеста, в своей фундаментальной работе "Чистый код" систематизировал принципы написания эффективного, поддерживаемого кода.

Книга "Чистый код" это детально проработанная методология, основанная на десятилетиях опыта разработки коммерческих систем. Она отвечает на ключевые вопросы:

  • Как создать код, который понятен коллегам через месяцы после написания?

  • Какие паттерны предотвращают накопление технического долга?

  • Как проектировать компоненты для лёгкого тестирования и модификации?

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

Что такое чистый код?

Мартин начинает с фундамента: чистый код - это код, написанный с заботой о читателе (часто будущем вас или коллеге). Этот код:

  • Прямолинеен: Делает ровно то, что ожидается.

  • Лаконичен: Ничего лишнего. Одна операция - одна функция, одна ответственность - один класс.

  • Выразителен: Имена и структура и без комментариев объясняет, что делается и почему.

  • Поддерживаем: Легко изменять и расширять без страха сломать что-то ещё.

  • Проверен: Покрыт осмысленными юнит-тестами.

Основа всего - имена

Правильные названия - 90% чистого кода. Вот основные правила:

  1. Выразительность: Имя должно отвечать на все главные вопросы: Что это? Зачем нужно? Как используется? Избегайте общих слов (data, info, manager), если они не полностью говорят о смысле.

    • Плохо: int d; // elapsed time in days

    • Хорошо: int elapsedTimeInDays;

    • Плохо: List list1;

    • Хорошо: List activeCustomers;

  2. Избегайте дезинформации:
    Не используйте accountList для переменной типа Account[] (это не список). Лучше accounts или accountGroup.
    Не используйте похожие имена: XYZControllerForEfficientHandlingOfStrings и XYZControllerForEfficientStorageOfStrings.

  3. Делайте осмысленные различия:

    • Плохо: ProductProductInfoProductData (что отличает их?).

    • Плохо: a1, a2, ... aN.

    • Хорошо: sourceCustomertargetCustomeroriginalMessageencryptedMessage.

  4. Используйте произносимые имена: genymdhms (generate date, year, month, day, hour, minute, second) - кошмар. Лучше generationTimestamp.

  5. Используйте поисковые имена: Однобуквенные имена (ijk в коротких циклах - исключение) и числовые константы (15) сложно найти в коде.

    • Плохо: if (employee.Flags & 15) ... 

    • Хорошо: const int IsFullTimeFlag = 0x01; const int IsOnProbationFlag = 0x02;

  6. Классы / Типы: Имена существительных или существительных с уточнением (CustomerAddressParserAccountService). Избегайте ManagerProcessorDataInfo.

  7. Методы: Имена глаголов или глагольных фраз (Save(), Delete(), ParseConfiguration(), CalculateTotal()). Геттеры/сеттеры - GetName(), SetName().

  8. Булевы переменные / методы: Используйте префиксы is, has, can, should для ясности: isActive, hasLicense, canExecute, shouldValidate.

Методы и функции

Маленькие, хорошо организованные методы это главное чистого кода!

  1. МАЛЕНЬКИЕ! И ещё раз МАЛЕНЬКИЕ!

    • Идеал: Не длиннее 20 строк. Часто 3-4 строки.

    • Правило экрана: Функция должна полностью помещаться на одном экране без прокрутки.

  2. Делайте одно дело (The Single Responsibility Principle - SRP для функций):

    • Функция должна делать ТОЛЬКО то, что явно следует из ее имени. Если вы можете выделить другую операцию с осмысленным именем - вынесите ее в отдельную функцию.

    • Уровни абстракции: Операции внутри функции должны быть на ОДНОМ уровне абстракции. Не смешивайте высокоуровневую логику (ProcessOrder()) с низкоуровневыми деталями (ParseOrderLineItemString()).

  3. Структура кода (Сверху-Вниз - Stepdown Rule):

    • Код должен читаться как повествование, сверху вниз.

    • На верхнем уровне высокоуровневые функции (шаги алгоритма).

    • Каждый следующий уровень - более детальные функции, реализующие шаги верхнего уровня.

  4. Аргументы Функций (Параметры):

    • Идеал: 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)

  5. Глаголы и ключевые слова:

    • Один аргумент: Имя функции и аргумента должны образовывать естественную пару глагол/существительное: WriteField(name)AssertExpectedEqualsActual(expected, actual).

    • Два аргумента: Порядок должен следовать общепринятому: Point p = new Point(x, y);.

  6. Исключения вместо кодов ошибок:

    • Плохо: Возврат кодов ошибок (int result = Save(); if (result == ERROR) ...) ведет к загромождению кода проверками сразу после вызова.

    • Хорошо: Бросайте исключения (try { Save(); } catch (SaveException ex) ...). Обработка ошибок отделена от основной логики.

    • Try[Операция]Паттерн: Для ситуаций, где ошибка это часть ожидаемого потока (например, проверка пароля), используйте паттерн TryParse:
      if (int.TryParse(input, out int value)) { ... } // Успех
      else { ... } // Ошибка, но не исключение

  7. DRY (Don't Repeat Yourself): Безжалостно устраняйте дублирование кода, выделяя общую функциональность в методы.

Пример рефакторинга функции (C#):
Грязный метод:

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

Комментарии

Лучший комментарий - отсутствующий комментарий. Хороший код объясняет себя сам через имена и структуру. Комментарии часто лгут (код меняется, а комментарии нет) и загромождают. Но есть исключения:

  1. Законные комментарии:

    • Правовые: Лицензии, авторские права.

    • Поясняющие намерение (WHY): Почему код сделан так, а не иначе? Особенно если причина неочевидна (баг в библиотеке, специфичное требование).

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

    • TODO: Краткие заметки о том, что нужно доделать / поправить позже. Обязательно указывайте контекст / причину.

    • Усиление (Amplification): Подчеркнуть важность чего-то неочевидного. // Не изменять порядок инициализации: компонент Y зависит от X!

    • Javadoc в Public API: Для публичных методов/классов библиотек - объяснение, что делает, параметры, возвращаемое значение, исключения.

  2. Недопустимые комментарии:

    • Закомментированный код: Удаляйте его! Системы контроля версий хранят историю.

    • Избыточные:

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

    • Скобочные / позиционные: // Конец метода CalculateTotal ------------------------

    • Комментарии-извинения: // Извините за этот баг...

    • Неясные ссылки: // См. замечание в мануале - Какой мануал? Какое замечание?

Форматирование

Единый стиль форматирования критически важен для читаемости.

  1. Вертикальное форматирование (Длина файла, пустые строки, группировка):

    • Длина файла: Стремитесь к маленьким файлам (200-500 строк). Большие классы — признак нарушения SRP.

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

    • Вертикальная Близость: Связанные концепции должны быть рядом. Переменная объявляется как можно ближе к месту использования. Вызываемые методы - ниже вызывающих (Правило Stepdown).

  2. Горизонтальное форматирование (Длина строки, отступы, пробелы):

    • Длина строки: 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 должен вызывать только методы:

    1. Самого объекта O (this).

    2. Объектов, переданных в M в качестве параметров.

    3. Объектов, созданных внутри M.

    4. Компонентов объекта O.

      • Не objectA.GetObjectB().DoSomething(); - Это "поездка по цепочке". Нарушает инкапсуляцию, делает код хрупким.

      • Решение: Делегирование. Пусть objectA предоставит метод, выполняющий нужное действие, скрыв свою зависимость от ObjectB:
        Плохо:


        Хорошо:

Обработка ошибок

Ошибки - часть жизни ПО. Обрабатывайте их чисто:

  1. Используйте исключения, а не коды ошибок.

  2. Сначала пишите Try-Catch-Finally: Обрабатывайте код, который может выбросить исключение, блоком try. Обрабатывайте конкретные исключения в catch. Освобождайте ресурсы в finally.

  3. Не возвращайте null! Это источник NullReferenceException (для C#, аналогично в других языках). Возвращайте пустые коллекции (Enumerable.Empty(), new List()), используйте Nullable<T> для значимых типов, применяйте паттерн Null Object.

  4. Не передавайте null! Это смещает ответственность за проверку на вызывающего. Бросайте ArgumentNullException в начале метода, если null недопустим.

Использование стороннего кода

Работа с библиотеками/API требует аккуратности:

  1. Изолируйте границы: Не позволяйте стороннему коду "расползаться" по всей вашей кодовой базе. Оберните его в адаптер (Adapter или Facade паттерн).

  2. Изучайте через тесты: Напишите небольшие юнит-тесты для сторонней библиотеки перед интеграцией. Это поможет понять её поведение и защитит от неожиданных изменений при обновлении.

Юнит-Тесты. TDD и чистота тестов

Без тестов нет чистого кода. Тесты обеспечивают безопасность рефакторинга.

  1. TDD (Test-Driven Development) цикл:

    1. Красный: Напишите маленький тест для новой функциональности. Он должен упасть (так как функциональности ещё нет).

    2. Зелёный: Напишите минимальный код, чтобы тест прошёл. Можно схитрить.

    3. Рефакторинг (Синий): Улучшите структуру кода (как продакшн, так и тест), удалите дублирование, примените принципы чистого кода. Тесты должны оставаться зелёными.

  2. Правила чистых тестов (F.I.R.S.T.):

    • F (Fast): Тесты должны выполняться мгновенно (секунды, а не минуты). Медленные тесты не запускают часто.

    • I (Independent): Тесты не должны зависеть друг от друга. Порядок выполнения не важен. Сброс состояния перед каждым тестом.

    • R (Repeatable): Результат теста одинаков в любой среде (dev, CI, продакшн) и при любом количестве запусков. Нет зависимостей от сети, времени, случайных чисел.

    • S (Self-Validating): Тест должен выдать бинарный результат: УСПЕХ или ПРОВАЛ. Нет ручной проверки логов.

    • T (Timely): Пишите тесты либо до написания кода (в подходе TDD), либо немедленно после написания рабочего кода. Никогда не откладывайте написание тестов на потом!

  3. Качество тест-кода: Так же важно, как и продакшн код. Применяйте все правила чистого кода: выразительные имена, маленькие функции, минимальные утверждения (Asserts) на тест, отсутствие дублирования (используйте SetUp / TearDown или фабричные методы аккуратно), ясность (Build-Operate-Check паттерн).

Классы

  1. Принцип единственной ответственности (Single Responsibility Principle - SRP): У класса должна быть только одна причина для изменения. Если класс делает слишком много, его нужно разбить.
    Признаки нарушения SRP: Много несвязанных методов/полей, частые изменения в разных местах класса по разным причинам, большой размер.

  2. Инкапсуляция: Скрывайте данные и детали реализации! Делайте поля приватными. Предоставляйте доступ через методы / свойства. Ослабляйте уровень доступа только при явной необходимости.

  3. Композиция прежде наследования (Composition over Inheritance): Наследование создаёт сильную связь между классами. Часто композиция (включение одного класса в другой как поле) + интерфейсы дают больше гибкости.

    • Плохо:

    • Хорошо:

Системы

Чистота на уровне архитектуры.

  1. Разделение забот (Separation of concerns): Система должна быть разделена на слабо связанные модули (например, по слоям: UI, бизнес-логика, доступ к данным). Каждый модуль решает свою задачу.

  2. Чистая архитектура (Clean Architecture/Onion/Hexagonal): Основная идея - зависимости направлены внутрь, к ядру. Бизнес-правила (самые стабильные) в центре. Инфраструктура (БД, UI, фреймворки - часто меняющиеся) на периферии. Ядро не зависит от деталей реализации периферии. Достигается через интерфейсы и DI.

  3. Внедрение зависимостей (Dependency Injection - DI, дополнительно Zenject): Классы не создают свои зависимости, а получают их извне (через конструктор, свойства, методы). Это ослабляет связность, упрощает тестирование и делает код гибче.

  4. Масштабирование: Система должна начинаться с простой, чистой архитектуры. Не добавляйте сложности "на вырост"! Рефакторите и расширяйте архитектуру по мере реальной необходимости.

Пример рефакторинга

Мартин детально разбирает эволюцию небольшой программы (генератор простых чисел) от грязной первой версии до чистого кода. Этапы:

  1. Начало: Рабочая, но ужасно написанная версия.

  2. Написание тестов: Покрытие функциональности тестами для безопасности изменений.

  3. Рефакторинг волнами:

    • Разбиение огромного метода на маленькие.

    • Улучшение имён переменных и функций.

    • Устранение дублирования.

    • Упрощение логики.

    • Улучшение алгоритма.

  4. Результаты: Код становится в разы короче, понятнее, эффективнее и легче для изменения.

Словарик терминов

  1. Чистый код (Clean code) - Код, который легко читать, понимать и изменять. Обладает свойствами: прямолинейность, лаконичность, выразительность, поддерживаемость, проверенность.

  2. Выразительность (Expressiveness) - Свойство кода, при котором имена переменных / методов / классов и структура однозначно передают их назначение и логику без комментариев.

  3. DRY (Don't Repeat Yourself) - Принцип разработки "Не повторяйся". Любое знание или логика должны иметь единственное представление в системе.

  4. SRP (Single Responsibility Principle) - Для функций/классов: Объект должен иметь только одну причину для изменения (выполнять одну обязанность).

  5. Инкапсуляция (Encapsulation) - Сокрытие внутреннего состояния объекта и деталей реализации. Доступ к данным только через публичные методы / свойства.

  6. Закон Деметры (Law of Demetr / Principle of Least Knowledge) - Метод объекта A должен взаимодействовать только с:

    • Самим A

    • Параметрами, переданными в метод

    • Объектами, созданными внутри метода

    • Непосредственными компонентами A. Запрещает цепочки вызовов: objA.GetB().DoC()

  7. Исключение (Exception) - Механизм обработки ошибок, прерывающий нормальный поток выполнения и передающий управление обработчику (catch). Альтернатива кодам ошибок.

  8. Null-объект (Null Object Pattern) - Паттерн, подразумевающий возврат специального объекта с нейтральным поведением вместо null (например, пустая коллекция).

  9. Адаптер (Adapter Pattern) - Паттерн для преобразования интерфейса класса в другой интерфейс, ожидаемый клиентом. Используется для изоляции стороннего кода.

  10. Фасад (Facade Pattern) - Паттерн, предоставляющий простой интерфейс к сложной подсистеме. Скрывает её детали реализации.

  11. Юнит-тест (Unit Test) - Автоматизированный тест, проверяющий корректность работы небольшой изолированной части кода (метода, класса).

  12. TDD (Test-Driven Development) - Методология разработки:
    🔴 Red → Написать падающий тест
    🟢 Green → Написать минимальный код для прохождения теста
    🔵 Refactor → Улучшить код, сохраняя зелёный статус.

  13. F.I.R.S.T. - Принципы чистых тестов:

    • Fast - Быстрые

    • Independent - Независимые

    • Repeatable - Повторяемые

    • Self-Validating - Самопроверяющиеся

    • Timely - Своевременные.

  14. Arrange-Act-Assert (AAA) - Паттерн структуры юнит-теста:

    • Arrange: Подготовка данных и зависимостей

    • Act: Вызов тестируемого метода

    • Assert: Проверка результата.

  15. Внедрение зависимостей (DI, Dependency Injection) - Передача зависимостей объекта извне (через конструктор / свойства) вместо их создания внутри. Уменьшает связанность.

  16. Чистая архитектура (Clean Architecture) - Архитектурный подход, где:

    • Бизнес-логика (ядро) не зависит от деталей (БД, UI, фреймворков)

    • Зависимости направлены к центру системы

  17. Разделение забот (SoC, Separation of Concerns) - Принцип разделения программы на независимые модули, каждый из которых решает отдельную задачу (например: UI, бизнес-логика, БД).

  18. Композиция (Composition) - Построение функциональности за счёт включения одних объектов в другие (предпочтительнее наследования).

  19. Рефакторинг (Refactoring) - Изменение структуры кода без изменения его поведения. Цель - улучшение читаемости и упрощение поддержки.

  20. Непрерывный рефакторинг (Continuous Refactoring) - Практика постоянного улучшения кода в процессе разработки.

  21. Дурной запах кода (Code Smell) - Симптом проблемы в коде (например: длинный метод, дублирование, "божественный класс").

  22. Уровень абстракции (Level of Abstraction) - Степень детализации операций. Код внутри функции должен оперировать на одном уровне (например, только бизнес-логика или только технические детали).

  23. Правило пошагового спуска (Stepdown Rule) - Код должен читаться сверху вниз: высокоуровневая логика -> детали реализации. Каждый уровень раскрывает подробности предыдущего.

  24. Искажение (Misinformation) - Использование имён, вводящих в заблуждение (например: accountList для массива).

  25. Обучающие тесты (Learning Tests) - Тесты, написанные для изучения поведения сторонней библиотеки перед её использованием в проекте.

  26. Границы (Boundaries) - Места интеграции с внешними системами (библиотеки, API, устаревший код). Требуют изоляции.

  27. Модуль (Module) - Логически связанная группа функций / классов (например: компонент, слой, библиотека).

  28. Связанность (Coupling) - Степень зависимости между модулями. Низкая связанность - признак хорошего дизайна.

  29. Зацепление (Cohesion) - Мера связанности обязанностей внутри модуля. Высокое зацепление - все элементы модуля работают на единую цель.

  30. Искажение абстракции (Abstraction Leakage) - Ситуация, когда детали реализации просачиваются через абстракцию, нарушая инкапсуляцию.


Если понравилась статья - рекомендую подписаться на телеграм‑канал NetIntel. Там вы сможете найти множество полезных материалов по IT и разработке!

Теги:
Хабы:
+2
Комментарии40

Публикации

Ближайшие события