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.
Имя — не косметика. Это гипотеза об ответственности класса. Неточное имя — неточная ответственность. Отсюда: разросшиеся классы, размытые границы, страх изменений.
Weasel Words: имена ни о чём
Слова, которые ничего не сужают. Красные флаги в 90% случаев.
Manager
UserManager, OrderManager, ConnectionManager. Что делает? Управляет. Как? Не сказано.
Практически: сваливаем в него всё, что «про пользователей». Cohesion — coincidental.
Что делать: уточнить ответственность. UserManager → UserRegistrar (создание) + UserAuthenticator (auth) + UserProfileUpdater (профиль).
Helper / Utility / Utils
StringHelper, DateUtils, FileUtility. «Полезные функции для работы с X».
Проблемы:
- Магнит для случайного кода. Через год там 200 функций, никто не знает что где.
- Обычно static. Процедурный код в OOP-обёртке.
- Testing одного использования тянет всю зависимость.
Что делать: конкретные операции живут ближе к своему использованию или в маленьких focused-модулях. DateUtils.parseIso8601 → dateparser.parse_iso8601 в модуле dateparser с одной ответственностью.
Handler / Processor
OrderHandler.handle_order(), MessageProcessor.process_message(). Что именно происходит? Confirm? Cancel? Retry?
Что делать: конкретный глагол в бизнес-домене. OrderHandler → ConfirmOrderCommandHandler, 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 · закрепить
Проверьте себя
Q1. Класс UserManager с 30 методами — это признак...
Q2. Feature Envy — это когда...
Q3. Какие имена — типичные weasel words?
Q4. Длинное имя ConfirmOrderCommandHandler — это...
Q5. Primitive Obsession — это когда...