Чистый код: имена, функции, границы, комментарии
Есть неудобный факт, вокруг которого крутится вся эта тема: код читают гораздо чаще, чем пишут. Оценки соотношения гуляют — от 10:1 у Роберта Мартина до более скромных цифр в исследованиях, — но знак неравенства не меняется никогда. Разработчик, который «пишет код», большую часть рабочего дня проводит в режиме reverse engineering: открывает файл, восстанавливает в голове модель происходящего, вносит три строки, закрывает файл. Чем дольше живёт проект, тем сильнее доминирует эта фаза.
Отсюда рабочее определение, которым мы будем пользоваться дальше:
Чистый код — это код, который минимизирует стоимость восстановления контекста у следующего читателя. Не код, который красиво выглядит; не код, где функции короче пяти строк; не код, прошедший линтер. Читатель — это метрика.
В статье https://courses.digitable.life/post/principles/00-overview/ мы разложили стоимость изменения на слагаемые
C_find + C_understand + C_edit + C_verify + C_risk. Всё, что называют «чистым кодом», бьёт
ровно в два из них: C_understand и C_find. Это полезно помнить, потому что практики чистого
кода не бесплатны — они торгуют временем автора и иногда производительностью машины за время
читателя. Когда читателей нет (скрипт на выброс, прототип на неделю), сделка невыгодна.
Ниже — четыре большие темы: имена, функции, границы и комментарии. Плюс отдельный раздел с критикой, потому что «Clean Code» Роберта Мартина — книга одновременно очень влиятельная и очень спорная, и знать возражения к ней так же важно, как знать её тезисы.
1. Имена
1.1 Интуиция: имя — это сжатый контракт
Фил Карлтон однажды сказал, что в информатике всего две сложные вещи: инвалидация кэша и придумывание
имён (Fowler, TwoHardThings). Шутка держится
на том, что имя — это не ярлык, а сжатие. Когда вы пишете activeSubscriptions, вы обещаете
читателю: это коллекция, элементы — подписки, все они активны. Читатель принимает обещание на веру
и не идёт смотреть реализацию. Если обещание ложно (внутри лежат ещё и подписки в grace-периоде),
вы заминировали код: следующий человек напишет корректную с виду строку, и она будет неверной.
Плохое имя дороже отсутствующего. Отсутствие информации заставляет читать код — это стоит времени. Ложная информация заставляет читать код позже, уже после инцидента — это стоит денег.
1.2 Длина имени пропорциональна размеру области видимости
Самое практичное правило про имена сформулировал не Мартин, а Роб Пайк в «Notes on Programming in C» (1989) и позже — Кернигана и Пайка в «The Practice of Programming»:
Длина имени должна быть примерно пропорциональна размеру области видимости, в которой оно живёт.
Здесь важно, что зависимость двусторонняя. Однобуквенное i в трёхстрочном цикле — идеально:
читатель видит объявление и использование одним взглядом, дополнительные буквы только шумят.
Однобуквенное d в публичном поле класса — катастрофа. Но и наоборот: indexOfCurrentIterationItem
в трёхстрочном цикле — это не «более чистый» код, а лишние 25 символов, которые нужно прочитать
и отбросить.
Обратите внимание: у Мартина в «Clean Code» правило сформулировано в одну сторону («длинное имя — это нормально»), у Пайка — в обе. Двусторонняя версия практичнее, и именно она закреплена в стилевых руководствах Go: Go Code Review Comments прямо говорит, что чем дальше от объявления используется переменная, тем описательнее должно быть имя.
1.3 Каталог типовых ошибок в именах
# ДЕЗИНФОРМАЦИЯ: это не список, а dict, и не все аккаунты
accountList = {acc.id: acc for acc in accounts if acc.is_active}
active_accounts_by_id = {acc.id: acc for acc in accounts if acc.is_active} # так
# ШУМОВЫЕ СЛОВА: Data, Info, Manager, Processor, Helper не добавляют смысла
class UserDataManager: ...
class UserRepository: ... # так: имя отвечает, чем объект является
# МАГИЧЕСКИЕ ЧИСЛА: условие невозможно прочитать вслух
if user.role == 3 and order.total > 10000:
apply_manual_review(order)
MANUAL_REVIEW_THRESHOLD_RUB = 10_000 # так
if user.role is Role.COMPLIANCE_OFFICER and order.total > MANUAL_REVIEW_THRESHOLD_RUB:
apply_manual_review(order)
Отдельная категория — непроизносимые и непоискиваемые имена. Имя genymdhms (generation year
month day hour minute second) нельзя обсудить голосом на созвоне, а односимвольное e нельзя найти
grep-ом. Поисковость — недооценённое свойство: на большом репозитории rg 'MANUAL_REVIEW_THRESHOLD'
находит все три места за секунду, а rg '10000' — четыреста ложных срабатываний.
1.4 Венгерская нотация: почему она умерла и почему частично права
Классическая венгерская нотация (strName, lpszBuffer, dwFlags) кодировала в имени тип. Она
умерла заслуженно: современные IDE показывают тип по наведению, а при рефакторинге имена перестают
соответствовать типам и начинают врать.
Но Джоэл Спольски в статье «Making Wrong Code Look Wrong»
показал, что исходная («апps») венгерская нотация Симони кодировала не тип, а вид — семантику,
которой в системе типов нет. usName (unsafe, от пользователя) против sName (safe, экранированная)
позволяет глазами увидеть XSS-уязвимость в строке write(usName).
Правильный современный вывод: не кодируйте семантику в префиксе — кодируйте её в типе.
// Вместо usName / sName — разные типы, которые компилятор не даст перепутать
type UnsafeHtml = string & { readonly __brand: 'UnsafeHtml' };
type SafeHtml = string & { readonly __brand: 'SafeHtml' };
function escape(input: UnsafeHtml): SafeHtml { /* ... */ }
function render(html: SafeHtml): void { /* ... */ }
const fromUser = req.body.comment as UnsafeHtml;
// render(fromUser); // ошибка компиляции — ровно то, что нужно
render(escape(fromUser)); // ок
Это тот же приём, что и Value Object в DDD: имя переменной может врать, тип — нет. Подробнее — в треке TypeScript.
1.5 Язык предметной области
Финальный уровень зрелости в именовании — когда код называет вещи так же, как их называет бизнес.
Если аналитик говорит «сторно», а в коде reverseTransactionV2 — на каждом обсуждении происходит
перевод, и на каждом переводе теряется точность. Единый язык (ubiquitous language) — центральная идея
Domain-Driven Design, и она даёт больше читаемости, чем все правила про длину функций вместе взятые.
2. Функции
2.1 Главный вопрос — не длина, а число переключений контекста
Знаменитый тезис «Clean Code»: функции должны быть маленькими, потом ещё меньше, 2–4 строки — норма. Этот тезис — самая цитируемая и самая неудачно сформулированная часть книги. Длина сама по себе не является причиной сложности; она коррелирует с ней.
Реальная причина, по которой длинная функция плохо читается, — смена уровня абстракции. Когда в одной функции соседствуют «применить скидку» и «записать 4 байта в буфер», читатель на каждой строке перестраивает мысленную модель: то он в терминах бизнеса, то в терминах сокета.
Правило, которое стоит запомнить вместо «функция ≤ 5 строк»:
Все инструкции внутри функции должны находиться на одном уровне абстракции. Функция из двадцати однородных строк читается легче, чем из шести разнородных.
Мартин Фаулер в заметке Function Length
формулирует это через «расстояние между намерением и реализацией»: извлекать фрагмент в функцию
стоит тогда, когда её имя объясняет зачем, а тело — как. Если имя получается вида
doStep2 — извлечение не окупилось.
2.2 Алгоритм принятия решения «извлекать ли»
объясняющее намерение?"} B -- нет --> Z["Оставить на месте:
извлечение только добавит прыжок"] B -- да --> C{"Фрагмент опирается на
> 3 локальные переменные?"} C -- да --> D{"Эти переменные —
связная сущность?"} D -- нет --> Z2["Сначала упростить состояние,
потом извлекать"] D -- да --> E["Ввести объект-параметр,
затем извлечь метод"] C -- нет --> F{"Фрагмент на другом
уровне абстракции?"} F -- да --> G["Извлечь: уровень выровняется"] F -- нет --> H{"Повторяется 3+ раз
с одинаковой причиной изменения?"} H -- да --> G H -- нет --> I["Извлекать необязательно;
решает читаемость"]
Обратите внимание на ветку «одинаковая причина изменения» — это прямая связь с DRY: три похожих фрагмента, которые меняются по разным причинам, объединять нельзя. Разбор этой ловушки — в статье https://courses.digitable.life/post/principles/02-dry-kiss-yagni/.
2.3 Аргументы: чем меньше, тем лучше — и почему
Число аргументов влияет на две вещи: сложность понимания вызова и сложность тестирования.
Функция от n булевых флагов имеет 2^n поведенческих веток, каждую из которых теоретически
нужно покрыть тестом. Отсюда практическое правило: флаг-аргумент почти всегда означает,
что внутри спрятаны две функции.
# ПЛОХО — вызов нечитаем: render(page, True, False, True) — что это значит?
def render(page: Page, inline_css: bool, minify: bool, with_toc: bool) -> str: ...
# ЛУЧШЕ — разные функции для разных намерений
def render_for_email(page: Page) -> str: ... # inline css, минификация, без оглавления
def render_for_web(page: Page) -> str: ...
# ИЛИ — объект-параметр с говорящими полями и разумными умолчаниями
@dataclass(frozen=True)
class RenderOptions:
inline_css: bool = False
minify: bool = True
with_toc: bool = False
def render(page: Page, options: RenderOptions = RenderOptions()) -> str: ...
Ещё один источник лишних аргументов — «выходные параметры» (append_to(report, buffer)): читатель
ожидает, что данные текут слева направо, а тут поток обратный.
2.4 Command–Query Separation
Принцип, введённый Бертраном Мейером в «Object-Oriented Software Construction»:
Метод либо изменяет состояние системы и ничего не возвращает (command), либо возвращает данные и ничего не меняет (query). Смешивать нельзя.
Нарушение CQS создаёт целый класс багов, потому что читатель не ожидает, что «вопрос» имеет последствия.
# ПЛОХО: метод-вопрос, который втихую меняет состояние
class Session:
def is_authenticated(self) -> bool:
if self._token_expired():
self._refresh_token() # сюрприз: сетевой вызов и мутация внутри геттера
return self._token is not None
# Из-за этого невинная строка в логировании становится источником нагрузки:
log.debug("auth=%s", session.is_authenticated()) # уходит HTTP-запрос на каждый DEBUG-лог
# ХОРОШО: запрос отделён от команды
class Session:
def is_authenticated(self) -> bool: # query: чистая проверка
return self._token is not None and not self._token_expired()
def refresh(self) -> None: # command: явная мутация
self._token = self._auth_client.refresh(self._refresh_token)
Полезное следствие CQS — иерархия «чистоты» функций, по которой стоит осознанно двигать код:
чистая функция (нет I/O, нет мутаций, детерминирована) → локальная мутация (мутирует только своё) →
мутация аргументов и полей → внешний эффект (сеть, диск, время, random). Чем выше по этой лестнице
находится функция, тем дешевле её тест: чистая проверяется за микросекунды и безопасна в
конкурентности, функция с эффектом требует моков, падает по таймауту и не переиспользуется.
Практический приём — «функциональное ядро, императивная оболочка» (functional core, imperative shell): вся логика принятия решений живёт в чистых функциях, а весь I/O — в тонком внешнем слое. Это резко удешевляет тесты, что подробно разбирается в https://courses.digitable.life/post/principles/06-testing-principles/, и напрямую связано с идеями трека https://courses.digitable.life/post/paradigms/00-overview/.
2.5 Как измерять сложность функции численно
Есть две метрики, которые реально используют в CI.
Цикломатическая сложность (McCabe, 1976) — число независимых путей в графе управления:
M = E − N + 2P, где E — рёбра, N — узлы, P — компоненты связности. Практически:
1 + количество if, for, while, case, and/or, catch. Это нижняя граница числа тестов
для покрытия всех ветвей. Порог 10 на функцию — общепринятая точка, где стоит насторожиться.
Когнитивная сложность (SonarSource,
whitepaper) — попытка измерить не
число путей, а трудность чтения: вложенность штрафуется сильнее, чем последовательность.
Плоская цепочка из десяти if-ов, каждый из которых делает return, имеет высокую цикломатическую
сложность, но низкую когнитивную — и читается действительно легко.
# Цикломатическая ~5, когнитивная ~10: каждый уровень вложенности штрафуется +1 к базе
def price(order, user, coupon):
if order: # +1
if user.is_premium: # +2 (вложенность 1)
if coupon: # +3 (вложенность 2)
return order.total * 0.7
else:
return order.total * 0.8
else:
if coupon: # +3
return order.total * 0.9
return order.total
# Цикломатическая та же ~5, когнитивная ~4: guard-и вместо вложенности
def price(order, user, coupon):
if not order: # +1
return 0
discount = 1.0
if user.is_premium: # +1
discount -= 0.2
if coupon: # +1
discount -= 0.1
return order.total * discount
Второй вариант к тому же чинит скрытый баг первого: ветка «не премиум, без купона» там возвращала
order.total через нижний return, что легко упустить глазами.
Trade-off, о котором молчат: метрики сложности легко обмануть. Если поставить в CI жёсткий
порог cognitive-complexity ≤ 8, команда начнёт механически резать функции на _step_1, _step_2,
и суммарная сложность системы вырастет — она просто переедет из тела функции в граф вызовов.
Метрика полезна как триггер для разговора на ревью, а не как автоматический блокер.
3. Границы
3.1 Зачем изолировать чужой код
Граница — это место, где ваш код встречается с кодом, который вы не контролируете: HTTP-клиент платёжного провайдера, ORM, брокер сообщений, SDK облака, чужой микросервис. У чужого кода есть свойство, которого нет у вашего: он меняется по чужому расписанию и в чужих интересах.
Симптом отсутствия границы: import stripe встречается в 47 файлах, включая доменные сущности.
В день, когда Stripe выпускает мажорную версию SDK и переименовывает Charge в PaymentIntent,
проект встаёт на две недели.
Приём называется anti-corruption layer (термин из DDD) или просто «адаптер»: чужая модель не проникает внутрь, на границе она переводится в вашу.
(чистая логика) participant P as Порт
PaymentGateway (интерфейс) participant A as Адаптер
StripeGateway participant S as Stripe API D->>P: charge(order_id, Money(1500, "RUB")) Note over D,P: домен знает только свой тип Money
и свои ошибки P->>A: тот же вызов, конкретная реализация A->>A: перевести Money → amount в копейках + currency A->>S: POST /v1/payment_intents S-->>A: 402 card_declined / 429 rate_limited A->>A: перевести чужую ошибку в доменную
PaymentDeclined / TemporaryFailure A-->>D: PaymentDeclined(reason=INSUFFICIENT_FUNDS) Note over A,S: всё знание о Stripe заперто здесь;
смена провайдера = новый адаптер
# --- Домен: ничего не знает про Stripe -------------------------------
class PaymentDeclined(Exception): ...
class TemporaryPaymentFailure(Exception): ...
class PaymentGateway(Protocol):
"""Порт. Определён на стороне домена — это важно для DIP."""
def charge(self, order_id: str, amount: Money) -> PaymentReceipt: ...
# --- Инфраструктура: единственное место, где живёт слово stripe -------
class StripeGateway:
def __init__(self, client: "stripe.Client") -> None:
self._client = client
def charge(self, order_id: str, amount: Money) -> PaymentReceipt:
try:
intent = self._client.payment_intents.create(
amount=amount.minor_units, # наш Money → их копейки
currency=amount.currency.lower(),
metadata={"order_id": order_id},
)
except stripe.error.CardError as exc: # чужая ошибка не течёт наружу
raise PaymentDeclined(str(exc)) from exc
except stripe.error.RateLimitError as exc:
raise TemporaryPaymentFailure(str(exc)) from exc
return PaymentReceipt(external_id=intent.id, captured=intent.status == "succeeded")
Обратите внимание, что интерфейс PaymentGateway объявлен на стороне домена, а не библиотеки —
это Dependency Inversion в чистом виде, см. https://courses.digitable.life/post/principles/01-solid/. И заметьте, что мы
переводим не только данные, но и ошибки: пропуск этого шага — самая частая протечка границы.
Подробно про доменные ошибки — в https://courses.digitable.life/post/principles/08-errors-and-resilience/.
3.2 Learning tests: как изучать чужую библиотеку
Отличная практика из «Clean Code», которую почему-то редко применяют. Вместо того чтобы читать документацию и надеяться, вы пишете тесты на чужую библиотеку — короткие, проверяющие ровно те сценарии, которые нужны вам.
# tests/learning/test_redis_semantics.py
# Это не тесты нашего кода, а исполняемая документация про поведение Redis,
# от которого мы зависим. Обновление клиента прогоняет эти тесты первыми.
def test_setnx_на_существующем_ключе(redis):
assert redis.set("lock:42", "a", nx=True) is True
assert redis.set("lock:42", "b", nx=True) is None # не False, а None — важная деталь!
def test_ttl_на_несуществующем_ключе(redis):
assert redis.ttl("нет-такого") == -2 # -2 = ключа нет, -1 = нет TTL
Ценность двойная: вы фиксируете своё понимание в исполняемом виде, а при обновлении версии зависимости эти тесты падают первыми и точечно показывают, что именно изменилось в семантике. Без них вы узнаете об изменении на проде.
3.3 Границы внутри системы
Границы бывают не только с внешним миром. Любой модуль, который вы объявляете «своим API для остальной команды», — это граница, и к ней применимо то же правило: наружу выставляем минимум.
package billing
// Ledger — единственная публичная точка входа в пакет.
type Ledger interface {
Issue(ctx context.Context, orderID string, amount Money) (InvoiceID, error)
Void(ctx context.Context, id InvoiceID, reason string) error
}
func NewLedger(db *sql.DB, clock Clock) Ledger { /* ... */ }
// Приватная логика физически недоступна извне пакета —
// значит, её можно переписывать без согласования с кем бы то ни было.
func (l *ledger) applyTaxRules(inv *invoice) error { /* ... */ }
Go делает это на уровне языка (регистр первой буквы), в Java есть модули JPMS, в C# — internal,
в Python приходится договариваться через _-соглашение и __all__. Механизм разный, идея одна:
узкий публичный интерфейс — это свобода менять всё остальное. Подробнее — в
https://courses.digitable.life/post/principles/03-coupling-and-cohesion/ и в треке Go.
4. Комментарии
4.1 Исходная позиция: комментарий — признак неудачи выразительности
Мартин формулирует жёстко: «Every comment is a failure to express yourself in code». Формулировка чрезмерная, но зерно верное — большинство комментариев, которые встречаются в реальном коде, это пересказ следующей строки:
# увеличиваем счётчик на единицу
counter += 1
# проверяем, что пользователь активен
if user.is_active:
Такие комментарии не просто бесполезны — они вредны, потому что комментарии гниют. Код компилятор проверяет, комментарий — никто. Через три рефакторинга комментарий начинает описывать то, чего в коде уже нет, и активно вводит в заблуждение.
комментарий забыли Устаревший --> Врущий: код поменяли ещё раз,
смысл стал противоположным Врущий --> Инцидент: кто-то поверил комментарию Устаревший --> Актуальный: заметили на ревью Врущий --> Удалён: заметили на ревью Актуальный --> Удалён: код стал самоочевидным Удалён --> [*] note right of Врущий Худшее состояние системы: дороже отсутствия комментария, потому что ему верят end note
Вывод: чем ближе комментарий к описанию механики («что делает код»), тем быстрее он сгниёт, потому что механика меняется чаще всего.
4.2 Но абсолютизм здесь вреден
Джон Оустерхаут в «A Philosophy of Software Design» занимает прямо противоположную позицию и аргументирует её убедительно: код физически не способен выразить всё, что нужно знать читателю. Он не выражает намерение, отвергнутые альтернативы, инварианты, единицы измерения, ссылки на внешние причины. Оустерхаут и Мартин публично разобрали свои разногласия в открытом диалоге aposd-vs-clean-code — это один из лучших материалов по теме, читать целиком.
Рабочий синтез: комментарий должен содержать информацию, которой в коде нет и не может быть.
def retry_with_backoff(fn, attempts: int = 5):
"""
Экспоненциальный ретрай с джиттером.
Почему джиттер обязателен: без него все воркеры, упавшие на одном сбое БД,
просыпаются синхронно и добивают её повторным всплеском (thundering herd).
Инцидент INC-2481, 2025-03-14.
Почему именно 5 попыток: суммарное ожидание ~31 с укладывается в клиентский
таймаут 45 с. Увеличивать attempts без правки таймаута нельзя.
"""
for attempt in range(attempts):
try:
return fn()
except TemporaryPaymentFailure:
if attempt == attempts - 1:
raise
time.sleep(2 ** attempt * random.uniform(0.5, 1.5))
Ни одну из этих трёх вещей — причину джиттера, номер инцидента, связь константы с таймаутом — переименованием переменных выразить нельзя.
4.3 Таксономия: какие комментарии платят за себя
Что стоит писать почти всегда:
- Контракты и инварианты. Предусловия, постусловия, что гарантируется вызывающему. Это спецификация, она меняется реже реализации.
- «Почему», а не «что». Причина нетривиального решения, отвергнутые альтернативы.
- Предупреждения. «Этот метод держит блокировку таблицы; не вызывать в транзакции».
- Единицы измерения и системы координат.
timeout— это секунды или миллисекунды? Лучше выразить типом (timedelta,Duration), но если нельзя — комментарием. - Ссылки наружу. Номер RFC, ссылка на тикет, на баг в чужой библиотеке с воркэраундом.
Что нужно удалять без сожаления:
- Закомментированный код. Он есть в git. Единственная причина его существования — страх, а страх лечится тегом в истории, а не мусором в файле.
- История изменений в шапке файла (
// 2019-04-02 Иванов: добавил проверку). Это работа системы контроля версий; в шапке она всегда неполна и всегда врёт. - Закрывающие комментарии (
} // end for). Симптом слишком длинной функции — лечить надо функцию. - TODO без владельца и тикета. Такой TODO живёт вечно. Правило:
TODO(PROJ-1234): ...или удалить.
4.4 Комментарий как дезодорант
Отдельно стоит осознать частый паттерн: комментарий пишется в момент, когда автор чувствует, что код непонятен. Это верный сигнал — но реакция чаще всего неправильная. Прежде чем писать комментарий, задайте вопрос: можно ли устранить непонятность структурно?
# ПЛОХО — комментарий компенсирует условие, которое невозможно прочитать
# проверяем, что сотрудник имеет право на льготы:
# отработал 5+ лет ИЛИ полная занятость и не на испытательном
if (e.years > 5) or (e.hours >= 40 and e.status != 'probation'):
...
# ХОРОШО — условие само себя объясняет, комментарий не нужен
if employee.is_eligible_for_benefits():
...
class Employee:
def is_eligible_for_benefits(self) -> bool:
return self.is_long_tenured() or (self.is_full_time() and not self.is_on_probation())
Важный нюанс: это работает только если извлечённое имя точнее комментария. Если вместо
комментария появляется check_condition_1(), вы обменяли документацию на индирекцию и проиграли.
5. Формат и структура файла
Мелочь, которая заметно влияет на скорость чтения.
- «Газетная» структура. Наверху файла — самое общее (публичный API, точка входа), ниже — детали. Читатель, как в газете, читает заголовок и решает, углубляться ли.
- Вертикальная близость. Переменную объявляют рядом с первым использованием, а не в начале функции (наследие C89, где иначе было нельзя). Связанные функции — рядом; вызывающая выше вызываемой.
- Плотность и разрежение. Пустая строка — это знак препинания: она разделяет смысловые абзацы внутри функции. Отсутствие пустых строк читается как сплошной текст без точек.
- Форматирование — не предмет для дискуссии.
gofmt,black,prettier,dotnet format,mix formatвыбирают за вас. Любое время, потраченное командой на спор о скобках, — чистый убыток. Ставьте автоформат в pre-commit и в CI, обсуждение закрыто. Стандарты стиля разбираются в https://courses.digitable.life/post/principles/09-code-review-and-standards/.
6. Критика: где «Clean Code» ошибается
Честный разбор обязателен, иначе получится карго-культ, о котором предупреждает https://courses.digitable.life/post/principles/00-overview/.
1. Догма «функции по 2–4 строки» вредна. Доведённая до предела декомпозиция («extract till you drop») производит десятки крошечных методов, каждый из которых понятен, но целое непостижимо: чтобы понять один сценарий, нужно обойти пятнадцать функций в четырёх файлах. Оустерхаут называет это «classitis» и противопоставляет идею глубоких модулей: простой интерфейс, скрывающий существенную реализацию. Мелкие функции с мелкими интерфейсами — «мелкие модули» — увеличивают суммарную сложность системы, а не уменьшают.
2. Производительность игнорируется. Кейси Муратори в
«Clean Code, Horrible Performance»
измерил канонический «чистый» ООП-стиль (полиморфизм вместо switch, мелкие виртуальные вызовы)
и получил разницу в разы на вычислительной нагрузке: виртуальные вызовы мешают инлайнингу, рушат
предсказание переходов и убивают локальность. Возражение «преждевременная оптимизация — корень зла»
работает лишь частично: Кнут в «Structured Programming with go to Statements»
(1974) говорил о точечной микрооптимизации некритичного кода, а не о выборе стиля, который потом
нельзя изменить локально. Вывод: в бизнес-логике читаемость почти всегда важнее; во внутреннем цикле
рендера, кодека или парсера «грязный» плоский код нормален — важно, чтобы горячий код был
изолирован и снабжён бенчмарком.
3. Многие рекомендации не подтверждены эмпирически. Исследования связи стиля кода
с поддерживаемостью дают смешанные результаты; корреляция популярных метрик читаемости с реальной
скоростью изменений слабее, чем хотелось бы. Это не значит, что практики бесполезны — это значит,
что они эвристики, а не законы физики. Сюда же — критика примеров самой книги: финальный код
главы про Args-парсер регулярно разбирают за размер классов и скрытое состояние.
Альтернативная рамка. Дэн Норт предложил CUPID как замену SOLID и «чистоте»: Composable, Unix philosophy, Predictable, Idiomatic, Domain-based. Ключевой сдвиг — от «правильности» к «удобству в работе»: код хорош не когда соответствует списку правил, а когда с ним приятно и предсказуемо работать.
7. Как это выглядит в проде
Инженерные организации не полагаются на добрую волю — чистота встроена в конвейер:
| Механизм | Что даёт | Чем платите |
|---|---|---|
Автоформат в pre-commit (gofmt, black, prettier) |
ноль споров о стиле, чистые диффы | нужно один раз договориться о конфиге |
Линтеры с правилами именования (ruff, eslint, golangci-lint) |
ловит дезинформирующие и шумные имена | ложные срабатывания, нужен tuning |
| Метрики сложности в CI как warning, не как блокер | тема для разговора на ревью | легко обмануть механической нарезкой |
Обязательный TODO(TICKET) формат |
TODO не живут вечно | небольшая бюрократия |
| ADR (Architecture Decision Records) | «почему» живёт вне кода и не гниёт вместе с ним | нужно поддерживать дисциплину |
| Boy Scout Rule: тронул файл — оставь чуть чище | амортизированный рефакторинг без отдельных спринтов | требует культуры маленьких PR |
| Ротация ревьюеров | имена проверяются свежим взглядом | скорость ревью падает |
Отдельно про ADR (adr.github.io, формат Майкла Найгарда) — это лучший
ответ на проблему гниющих комментариев «почему». Решения архитектурного уровня («почему Kafka,
а не RabbitMQ», «почему идемпотентность на уровне ключа заказа») живут в docs/adr/0012-*.md,
а в коде остаётся одна строка-ссылка.
И замечание про измерение: единственная метрика чистоты, которая реально коррелирует с бизнесом, — это время от «взял задачу» до «в проде» для типовой правки. Если оно растёт квартал к кварталу при неизменном размере команды — код становится грязнее, что бы ни показывал дашборд линтера.
8. Чек-лист
Перед тем как отправить PR:
- Имена. Каждое имя переживёт grep? Длина соответствует области видимости? Нет ли имени, которое обещает больше или другое, чем содержит?
- Функции. Все строки одной функции на одном уровне абстракции? Есть ли флаг-аргументы? Нет ли метода-вопроса, который что-то меняет?
- Границы. Тип чужой библиотеки не просочился в домен? Чужие исключения переведены в свои?
- Комментарии. Каждый комментарий говорит то, чего код сказать не может? Нет закомментированного кода и TODO без тикета?
- Проверка на карго-культ. Для каждого извлечённого метода: его имя объясняет намерение лучше, чем сам код? Если нет — верните на место.
Мини-итог
- Чистый код измеряется не эстетикой, а стоимостью восстановления контекста у читателя.
- Длина имени пропорциональна размеру области видимости — в обе стороны.
- Главный критерий для функции — единый уровень абстракции, а не число строк; догма «2–4 строки» переносит сложность из тела функции в граф вызовов.
- Command–Query Separation и «функциональное ядро, императивная оболочка» дают больше, чем любые правила форматирования.
- На границе с чужим кодом переводите и данные, и ошибки; learning tests превращают обновление зависимости из лотереи в проверяемое событие.
- Комментарий полезен ровно в той мере, в какой содержит информацию, которую код выразить не может; всё остальное сгниёт и начнёт врать.
Источники
- Robert C. Martin. Clean Code, 2008; Kent Beck. Implementation Patterns, 2007.
- John Ousterhout. A Philosophy of Software Design, 2018 — страница книги; публичный спор с Мартином — aposd-vs-clean-code.
- Kernighan & Pike. The Practice of Programming, 1999.
- Steve McConnell. Code Complete, 2nd ed., 2004 — главы 11 (имена) и 32 (самодокументируемый код).
- Martin Fowler. Function Length, TwoHardThings.
- Joel Spolsky. Making Wrong Code Look Wrong; Casey Muratori. Clean Code, Horrible Performance; Dan North. CUPID.
- SonarSource. Cognitive Complexity; Go Code Review Comments; PEP 8; Google Style Guides; ADR.
Что дальше
Мы разобрали, как выглядит чистый код. Следующий шаг — научиться систематически распознавать грязный: у плохого кода есть повторяющиеся узнаваемые формы («запахи»), и почти для каждой существует отработанная механическая процедура исправления.