Принципы разработки Чистый код: имена, функции, границы, комментарии
0%

Чистый код: имена, функции, границы, комментарии

Чистый код: имена, функции, границы, комментарии

Есть неудобный факт, вокруг которого крутится вся эта тема: код читают гораздо чаще, чем пишут. Оценки соотношения гуляют — от 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 Алгоритм принятия решения «извлекать ли»

Обратите внимание на ветку «одинаковая причина изменения» — это прямая связь с 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) или просто «адаптер»: чужая модель не проникает внутрь, на границе она переводится в вашу.

# --- Домен: ничего не знает про 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:

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

Вывод: чем ближе комментарий к описанию механики («что делает код»), тем быстрее он сгниёт, потому что механика меняется чаще всего.

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:

  1. Имена. Каждое имя переживёт grep? Длина соответствует области видимости? Нет ли имени, которое обещает больше или другое, чем содержит?
  2. Функции. Все строки одной функции на одном уровне абстракции? Есть ли флаг-аргументы? Нет ли метода-вопроса, который что-то меняет?
  3. Границы. Тип чужой библиотеки не просочился в домен? Чужие исключения переведены в свои?
  4. Комментарии. Каждый комментарий говорит то, чего код сказать не может? Нет закомментированного кода и TODO без тикета?
  5. Проверка на карго-культ. Для каждого извлечённого метода: его имя объясняет намерение лучше, чем сам код? Если нет — верните на место.

Мини-итог

  • Чистый код измеряется не эстетикой, а стоимостью восстановления контекста у читателя.
  • Длина имени пропорциональна размеру области видимости — в обе стороны.
  • Главный критерий для функции — единый уровень абстракции, а не число строк; догма «2–4 строки» переносит сложность из тела функции в граф вызовов.
  • Command–Query Separation и «функциональное ядро, императивная оболочка» дают больше, чем любые правила форматирования.
  • На границе с чужим кодом переводите и данные, и ошибки; learning tests превращают обновление зависимости из лотереи в проверяемое событие.
  • Комментарий полезен ровно в той мере, в какой содержит информацию, которую код выразить не может; всё остальное сгниёт и начнёт врать.

Источники


Что дальше

Мы разобрали, как выглядит чистый код. Следующий шаг — научиться систематически распознавать грязный: у плохого кода есть повторяющиеся узнаваемые формы («запахи»), и почти для каждой существует отработанная механическая процедура исправления.

Запахи кода и каталог рефакторингов

Нашли неточность? Выделите фрагмент текста — рядом появится жучок.

Нужен разбор именно вашей ситуации?

Статья описывает общий случай. Если у вас частный — можно разобрать его отдельно, платно. А если не хватает целого материала, предложите тему: её оплачивают вскладчину, и она выходит открытой для всех.

Доска запросов