Комментарии: когда они помогают, а когда вредят
Комментарии - это как специи. Чуть-чуть - вкусно. Перебор - невозможно есть.
Главная проблема комментариев - они устаревают. Код меняется, комментарий остаётся. Через месяц он врёт. Через полгода - активно мешает.
Когда комментарии полезны
- объясняют почему, а не что
- предупреждают о неочевидном решении
- дают ссылку на баг/тикет/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();
}
Хороший комментарий отвечает на вопрос «почему так, а не иначе?» или предупреждает о ловушке.
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). Это даёт строгую типизацию без рантайма.
Закомментированный код - удаляй
// Плохо: мёртвый код, который «может пригодиться»
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 всё помнит