Уровень 1 · Фундамент Глава 02 9 мин

Naming и smells

Как имена сигнализируют о проблемах архитектуры. Manager, Helper, Utils — почему красные флаги. Feature Envy, Data Class, Shotgun Surgery.

TL;DR

  • Имя класса — гипотеза о его ответственности. Плохое имя = неточная гипотеза.
  • Manager, Helper, Utils, Data, Handler — почти всегда сигнал coincidental cohesion.
  • Feature Envy: метод в классе A часто обращается к данным класса B — должен быть в B.
  • Naming нельзя фиксить в отрыве от структуры: переименование часто вскрывает нужду в декомпозиции.

Почему имена важны

Открываете код: UserManager, OrderHelper, DataProcessor. Что делают? Ничего конкретного — они «обо всём про этот домен». Через полгода в каждом — 40 методов, все критичные, все связаны с чем-то, никто не помнит с чем именно.

There are only two hard things in Computer Science: cache invalidation and naming things.

Phil Karlton

Имя — не косметика. Это гипотеза об ответственности класса. Неточное имя — неточная ответственность. Отсюда: разросшиеся классы, размытые границы, страх изменений.

Weasel Words: имена ни о чём

Слова, которые ничего не сужают. Красные флаги в 90% случаев.

Manager

UserManager, OrderManager, ConnectionManager. Что делает? Управляет. Как? Не сказано.

Практически: сваливаем в него всё, что «про пользователей». Cohesion — coincidental.

Что делать: уточнить ответственность. UserManagerUserRegistrar (создание) + UserAuthenticator (auth) + UserProfileUpdater (профиль).

Helper / Utility / Utils

StringHelper, DateUtils, FileUtility. «Полезные функции для работы с X».

Проблемы:

  • Магнит для случайного кода. Через год там 200 функций, никто не знает что где.
  • Обычно static. Процедурный код в OOP-обёртке.
  • Testing одного использования тянет всю зависимость.

Что делать: конкретные операции живут ближе к своему использованию или в маленьких focused-модулях. DateUtils.parseIso8601dateparser.parse_iso8601 в модуле dateparser с одной ответственностью.

Handler / Processor

OrderHandler.handle_order(), MessageProcessor.process_message(). Что именно происходит? Confirm? Cancel? Retry?

Что делать: конкретный глагол в бизнес-домене. OrderHandlerConfirmOrderCommandHandler, CancelOrderCommandHandler. Один класс — одна ответственность.

Data / Info / Detail

UserData, OrderInfo, TransactionDetail. Просто «данные о X». Отличается от User/Order/Transaction — чем?

Что делать: либо это DTO (UserRegistrationRequest), либо Response модель (UserProfileResponse), либо просто User — «Data» ничего не добавляет.

Service

Спорный случай. UserService, AuthService, PaymentService. Иногда оправдано (Application Service, Domain Service), но часто — тот же Manager под другим именем.

Что делать: если это application service — назовите точнее, например RegisterUserUseCase. Если domain service — конкретная роль: PricingPolicy, ShippingCostCalculator.

Naming methods

Команды — императив, без возврата

order.confirm(), user.register(), payment.charge(amount). Меняют состояние, возвращают None или id/event.

Не order.confirmed() (это про query), не order.doConfirm() (do — филлер).

Query — существительное или is/has/can

order.total, user.email, product.is_active, customer.has_credit, subscription.can_be_cancelled().

Возвращают данные без побочных эффектов. По CQS.

Смешение — красный флаг

class OrderService:
    def get_and_lock_order(self, order_id):     # command + query!  
        order = self._repo.find(order_id)
        order.locked = True
        self._repo.save(order)
        return order

Одновременно query (get) и command (lock). Вызывающий думает, что просто читает.

Правильно: order.acquire_lock() явно меняет состояние. Или отдельные lock_order() и get_order().

Классические code smells

Feature Envy

Метод класса A постоянно обращается к данным класса B:

class Order:
    def total_with_customer_discount(self):
        base = sum(l.price for l in self.lines)
        discount = self.customer.tier_discount()             # !!  
        if self.customer.is_premium:                          # !!  
            discount += self.customer.premium_bonus            # !!  
        return base * (1 - discount)

Метод завидует полям Customer — постоянно к ним обращается. Значит, метод должен жить в Customer (или в PricingPolicy).

Правильно:

class Customer:
    def total_discount(self) -> Decimal:
        d = self.tier_discount()
        if self.is_premium:
            d += self.premium_bonus
        return d

# в Order:
def total(self, customer: Customer) -> Money:
    return self.subtotal * (1 - customer.total_discount())

Data Class

Класс из одних get/set без поведения:

class Order:
    id: int
    status: str
    total: Decimal

    def get_id(self): return self.id
    def set_id(self, v): self.id = v
    # ... 15 getter/setter'ов

Проблема: инкапсуляция нарушена, поведение размазано по внешним сервисам, инварианты не защищены. Классика Anemic Domain — см. главу 15.

Shotgun Surgery

Одно изменение требует править N файлов. «Добавили новое поле → правим DTO, ORM-модель, service, controller, mapper, validator, …, тесты».

Причина: логика поля размазана. Правильная организация — одно место контроля per поле (обычно aggregate).

Divergent Change

Один класс меняется по многим причинам. UserService меняется, когда меняется UI, БД, бизнес-правила, интеграция с email. Нарушение SRP.

Правильно: декомпозировать по причинам изменения.

Long Parameter List

Метод с 8 параметрами:

def create_report(user_id, from_date, to_date, format,
                  include_totals, group_by, filter_status, sort_field):

Параметры образуют кластер. Введите объект:

@dataclass(frozen=True)
class ReportQuery:
    user_id: UserId
    period: DateRange
    format: ReportFormat
    grouping: GroupingSpec
    filter: FilterSpec
    sort_field: SortField

def create_report(query: ReportQuery): ...

Primitive Obsession

Всё — str, int, float:

def send_notification(user_id: int, phone: str, message: str, priority: int): ...

Что там за phone? С кодом страны? Валидируется? Что за priority — 0 высший или низший?

Правильно: Value Objects. PhoneNumber, Message, Priority(Enum). Каждый инкапсулирует правила.

Naming aggregate

Aggregate — не «класс, обёртывающий таблицу». Он должен назвать бизнес-концепцию.

class OrderRow: ...       # ORM-строка
class OrderDTO: ...       # DTO для API
class OrderService: ...   # процедуры

# где Order? нет

Настоящее Order — aggregate с инвариантами — отсутствует. Только infrastructure-сущности.

Правильно: Order — существует в domain, знает про свои правила. Технические роли — с суффиксами: OrderRow (ORM), OrderResponse (API), но не отменяют Order как настоящее.

Naming в реальных проектах

Ubiquitous Language

Термины из главы 08: язык кода = язык бизнеса. Если аналитик говорит «клиент», а в коде user_id — разрыв языка.

Consistency важнее «красоты»

Плохо: одни методы find_by_id, другие getById, третьи retrieve_one. Читатель отвлекается на угадывание.

Правило: выбрали конвенцию — держитесь. Хоть find_by_id, хоть get_by_id — но одно из них последовательно.

Long names ≠ bad names

ConfirmOrderCommandHandler — длинно, но однозначно. Handler — коротко, но ни о чём.

Правило Мартина: имя должно точно передавать намерение. Если для этого нужно 30 символов — это нормально. Дешёвле длинного имени — сокращение при устоявшейся конвенции.

Что делать с плохими именами

Не переименовывать в вакууме. Плохое имя обычно симптом плохой ответственности.

    Заметили UserManager с 30 методами. Не переименовывайте в UserGodClass — так же плохо. Разберитесь: какие роли выполняет?

    Сгруппируйте методы по ответственностям. «Регистрация», «аутентификация», «управление профилем», «блокировки», «email-уведомления».

    Каждая группа = потенциальный отдельный класс.

    Извлеките один класс за коммит. AuthenticateUserUseCase наружу, UserManager меньше.

    Повторите для остальных ответственностей.

В конце UserManager пуст — удалите. Или он остался с одной ответственностью — переименуйте.

Тесты как проявитель имён

Плохое имя — плохой тест:

def test_processor_processes():                          # !!  
    processor = OrderProcessor(...)
    result = processor.process(order)
    assert result.status == 'processed'                  # !!  

Что тестирует? Что «процессор процессит»? Не понятно.

def test_confirms_order_when_pending_and_all_lines_reserved():
    order = OrderBuilder.pending_with_reserved_lines().build()
    order.confirm()
    assert order.status is OrderStatus.CONFIRMED

Тест читается как спецификация. Метод и класс — тоже.

В твоём же коде

В анонимизированном микросервисе есть примеры хороших имён и не очень:

Хорошее:

  • OrderStandardizeUseCase.execute() — глагол Standardize конкретен, execute — стандарт use case.
  • NormalizerPort.normalize() — интерфейс + операция.
  • PostgresItemRepository — технология + сущность.

Спорное:

  • handle_order_item — «handle» неопределённо. Лучше on_order_item_received (если это event handler) или явно receive_order_item (команда).
  • update_normalized_data(item_id, map) — CRUD-имя. Бизнесово было бы mark_items_as_normalized(...) — оно объясняет зачем, а не как.

Плохое: _adapt_for_consumer внутри use case — уже разбирали. Имя правильное для класса OutgoingMessageMapper, но не для приватного метода в use case.

Дальнейшее чтение

  • Martin Fowler. Refactoring: Improving the Design of Existing Code. Каталог smells (Feature Envy, Shotgun Surgery, Data Clumps, Primitive Obsession, Long Parameter List).
  • Robert C. Martin. Clean Code. Chapter 2 — Meaningful Names. Спорно местами, но канонично.
  • Sandi Metz. Practical Object-Oriented Design in Ruby. Про наименование ролей и границ.
  • Peter Hilton. How to Name Things. Отличный доклад про naming.

Проверьте себя

Мини-quiz · закрепить

Проверьте себя

  1. Q1. Класс UserManager с 30 методами — это признак...

  2. Q2. Feature Envy — это когда...

  3. Q3. Какие имена — типичные weasel words?

  4. Q4. Длинное имя ConfirmOrderCommandHandler — это...

  5. Q5. Primitive Obsession — это когда...