Комментарии: когда они помогают, а когда вредят

Комментарии - это как специи. Чуть-чуть - вкусно. Перебор - невозможно есть.

Главная проблема комментариев - они устаревают. Код меняется, комментарий остаётся. Через месяц он врёт. Через полгода - активно мешает.

Когда комментарии полезны

  • объясняют почему, а не что
  • предупреждают о неочевидном решении
  • дают ссылку на баг/тикет/RFC
  • описывают публичный API (godoc)

Когда комментарии вредят

  • описывают что делает код (это видно из кода)
  • устарели и врут
  • закомментированный код вместо удаления
  • «разделители» вместо функций

Плохие комментарии: примеры

// увеличиваем i на 1
i = i + 1

// получаем пользователя
user, err := repo.GetUser(ctx, id)

// цикл по элементам
for _, item := range items {
    process(item)
}
<?php
declare(strict_types=1);

// увеличиваем $i на 1
$i = $i + 1;

// получаем пользователя
$user = $repo->getUser($id);

// цикл по элементам
foreach ($items as $item) {
    process($item);
}

Все три комментария не несут информации - они повторяют код. Удали их, и ничего не потеряется.

Хорошие комментарии: примеры

// Временный костыль: внешний сервис иногда возвращает дубликаты,
// поэтому фильтруем по id. Убрать после FIX-123.
items = dedupe(items)

// Порядок вызовов важен: сначала кэш, потом БД.
// Если поменять - race condition при конкурентных запросах.
cached := cache.Get(key)
if cached == nil {
    cached = db.Get(key)
    cache.Set(key, cached)
}

// RFC 7519, Section 4.1.4: "exp" claim must be NumericDate
if claims.ExpiresAt.Before(time.Now()) {
    return ErrTokenExpired
}
<?php
declare(strict_types=1);

// Временный костыль: внешний сервис иногда возвращает дубликаты,
// поэтому фильтруем по id. Убрать после FIX-123.
$items = dedupe($items);

// Порядок вызовов важен: сначала кэш, потом БД.
// Если поменять - race condition при конкурентных запросах.
$cached = $cache->get($key);
if ($cached === null) {
    $cached = $db->get($key);
    $cache->set($key, $cached);
}

// RFC 7519, Section 4.1.4: "exp" claim must be NumericDate
if ($claims->expiresAt < new \DateTimeImmutable()) {
    throw new TokenExpiredException();
}

Хороший комментарий отвечает на вопрос «почему так, а не иначе?» или предупреждает о ловушке.

Если можешь заменить комментарий [переименованием](02-naming.md) функции или переменной - делай это. Код, который объясняет сам себя, лучше кода с пояснениями.

Godoc: комментарии как документация

В Go комментарии к exported-символам - это документация. Они отображаются в go doc и на pkg.go.dev:

// UserRepo provides access to user storage.
// It handles database operations and caching.
type UserRepo struct { ... }

// GetByID returns a user by their unique identifier.
// Returns ErrNotFound if the user does not exist.
func (r *UserRepo) GetByID(ctx context.Context, id int) (*User, error) {

В PHP роль godoc выполняет PHPDoc - блочные комментарии /** ... */ с тегами. Их читает IDE для автодополнения, PHPStan для статического анализа, OpenAPI-генераторы для документации:

<?php
declare(strict_types=1);

/**
 * Хранилище пользователей с кешем и БД.
 *
 * @see UserRepositoryInterface
 */
final readonly class UserRepository
{
    /**
     * Возвращает пользователя по уникальному идентификатору.
     *
     * @param non-empty-string $id внешний UUID пользователя
     *
     * @throws UserNotFoundException если пользователя не существует
     * @throws \RuntimeException     при ошибке сети с БД
     *
     * @deprecated since 2.5, use findById() instead
     */
    public function getById(string $id): User
    {
        // ...
    }
}

Правила PHPDoc и godoc:

Правило                                  PHPDoc                       Go (godoc)
─────────────────────────────────────    ──────────────────────────   ──────────────────────────
Описание поведения, не кода              «Возвращает пользователя»    `// GetByID returns...`
Тип ошибки явно                          @throws UserNotFoundException Returns ErrNotFound if...
Точные типы для PHPStan                  @param non-empty-string $id  -
Депрекация                               @deprecated since 2.5         // Deprecated: use Foo
Внешние ссылки                           @see, @link                  пакетная документация

Дополнительные теги PHPStan/Psalm: @param array{id: int, name: string} $data (shape types), @return list<User> (numeric arrays), @template T (generics). Это даёт строгую типизацию без рантайма.

Комментарии к приватным функциям пишутся по необходимости (когда есть «почему»). Godoc-стиль нужен только для exported-символов - именно они формируют публичный API пакета.

Закомментированный код - удаляй

// Плохо: мёртвый код, который «может пригодиться»
func ProcessOrder(order Order) error {
    // oldPrice := calculateOldPrice(order)
    // if oldPrice > 0 {
    //     applyDiscount(order, oldPrice)
    // }
    newPrice := calculatePrice(order)
    return charge(order, newPrice)
}

// Хорошо: удали. Git помнит всё.
func ProcessOrder(order Order) error {
    price := calculatePrice(order)
    return charge(order, price)
}
<?php
declare(strict_types=1);

// Плохо: мёртвый код, который «может пригодиться»
final class OrderProcessor
{
    public function process(Order $order): void
    {
        // $oldPrice = $this->calculateOldPrice($order);
        // if ($oldPrice > 0) {
        //     $this->applyDiscount($order, $oldPrice);
        // }
        $newPrice = $this->calculatePrice($order);
        $this->charge($order, $newPrice);
    }
}

// Хорошо: удали. Git помнит всё.
final class OrderProcessor
{
    public function process(Order $order): void
    {
        $price = $this->calculatePrice($order);
        $this->charge($order, $price);
    }
}

Закомментированный код - это шум. Он заставляет каждого читателя задуматься: «Это важно? Это нужно раскомментировать? Почему это здесь?». Git хранит историю - если код понадобится, его можно найти.

TODO и FIXME: когда допустимо

// TODO(dmitry): заменить на batch-insert после миграции на PostgreSQL 15
for _, user := range users {
    if err := repo.Create(ctx, user); err != nil {
        return err
    }
}

// FIXME: race condition при конкурентных запросах, нужен mutex
counter++
<?php
declare(strict_types=1);

// TODO(dmitry): заменить на batch-insert после миграции на PostgreSQL 15
foreach ($users as $user) {
    $repo->create($user);
}

// FIXME: race condition при конкурентных запросах, нужен distributed lock
$counter++;

Правила для TODO/FIXME:

Хорошо                               Плохо
──────────────────────────────────   ──────────────────────────────
TODO(автор): конкретное действие     TODO: fix this
TODO + ссылка на тикет               TODO: сделать лучше
FIXME с описанием бага               FIXME (без объяснения)

Комментарии-разделители → Extract Method

Если ты пишешь // - шаг 1 --- - это сигнал, что нужна маленькая функция:

// Плохо: комментарии-разделители
func HandleRequest(r *http.Request) error {
    // - parse input ---
    var input RequestInput
    json.NewDecoder(r.Body).Decode(&input)

    // - validate ---
    if input.Name == "" { return ErrNoName }

    // - save ---
    db.Create(&input)
    return nil
}

// Хорошо: функции вместо комментариев
func HandleRequest(r *http.Request) error {
    input, err := parseInput(r)
    if err != nil {
        return err
    }
    if err := validateInput(input); err != nil {
        return err
    }
    return saveInput(input)
}
<?php
declare(strict_types=1);

// Плохо: комментарии-разделители
final class RequestHandler
{
    public function handle(Request $request): void
    {
        // - parse input ---
        $input = json_decode($request->getContent(), associative: true, flags: JSON_THROW_ON_ERROR);

        // - validate ---
        if (($input['name'] ?? '') === '') {
            throw new \DomainException('name is required');
        }

        // - save ---
        $this->db->create($input);
    }
}

// Хорошо: методы вместо комментариев
final class RequestHandler
{
    public function handle(Request $request): void
    {
        $input = $this->parseInput($request);
        $this->validateInput($input);
        $this->saveInput($input);
    }
}

Названия функций - это самодокументирующиеся комментарии, которые компилятор проверяет.

Мини-задание

  • Найди 3 бесполезных комментария в проекте (описывают «что», а не «почему»)
  • Удали их или замени хорошими именами функций/переменных
  • Проверь godoc для exported-символов: начинаются ли с имени?
  • Найди и удали закомментированный код - Git всё помнит

Зарегистрируйтесь бесплатно, чтобы пройти квиз, решить задание с автопроверкой и вести прогресс.