Анемичные модели и примитивная одержимость
Две связанные темы сходятся в правиле MAPPER: данные и поведение предметной области должны жить вместе, а примитивы (string, int, decimal) — только там, где в реальности нет отдельного понятия.
Анемичная модель
Анемичная модель — классы-«контейнеры» с полями, геттерами и сеттерами, без инвариантов и доменных операций. Вся логика уезжает в *Service, *Manager, *Helper.
// Анемично: инвариант «сумма строк = total» нигде не закреплён
public class Order {
public List<OrderLine> Lines { get; set; } = new();
public decimal Total { get; set; }
}
public class OrderService {
public void Recalculate(Order order) {
order.Total = order.Lines.Sum(l => l.Price * l.Qty);
}
}
// Богаче: объект сам отвечает за согласованность
public sealed class Order {
private readonly List<OrderLine> _lines = new();
public Money Total { get; private set; }
public void AddLine(OrderLine line) {
_lines.Add(line);
Total = Money.Sum(_lines.Select(l => l.Subtotal));
}
}
Почему это запах:
- инкапсуляция формально есть, смысловой границы нет;
- любое изменение тянет правки в нескольких сервисах (связанность);
- тесты вынуждены поднимать половину приложения.
См. типы классов в DDD, ООП — инкапсуляция.
Практики против анемичности
| Симптом | Направление |
|---|---|
| Генераторы CRUD/DTO | Убрать автогенерацию «анемичных» сущностей |
| Пустой конструктор + сеттеры | Задавать обязательные поля при создании |
| Публичные сеттеры «на всё» | Операции addLine, cancel, pay вместо setState |
| DTO на каждый слой | DTO только на границе, внутри — доменные типы |
| Оргия мелких объектов | Слить по смыслу, а не по таблицам БД |
Примитивная одержимость (Primitive Obsession)
Одержимость элементарными типами — когда вместо понятий домена везде string, int, bool. Фаулер называет это Primitive Obsession; лечение — реификация: введение маленького типа с правилами.
// Примитивы: легко перепутать аргументы
function sendReceipt(email: string, amount: number, currency: string) { ... }
// Реификация
class Email {
constructor(private readonly value: string) {
if (!value.includes("@")) throw new Error("Invalid email");
}
}
class Money {
constructor(readonly amount: number, readonly currency: string) {}
}
Типичные кандидаты в value object:
| Вместо | Объект | Зачем |
|---|---|---|
string email, phone | Email, Phone | Валидация в одном месте |
double lat/lon | GeoCoordinate | Единицы и диапазоны |
string + string даты | DateInterval, Instant | Нет «31.02» в трёх слоях |
Map / JSON blob | Permissions, Tags | Явный API коллекции |
int статус | enum / state object | Исчерпывающие переходы |
Дополнительно: строки — не универсальный контейнер; ассоциативные массивы «на всякий случай» — тоже запах (лучше объект с именованными полями).
Value objects в DDD — доменная модель; пошаговая реификация — примитивы и маленькие типы; каталог приёмов — рефакторинг.
DTO и границы слоёв
DTO уместны на границе (HTTP, очередь, БД), когда контракт транспорта отделён от домена. Проблема начинается, когда DTO проникают внутрь и заменяют модель:
API DTO → Service копирует поля → ещё один DTO → Repository
Целевой поток:
API DTO → адаптер → доменный объект → use case
DTO только на границе — внутри домена остаются типы с поведением.
Связь с ORM и фреймворками
ORM и codegen по умолчанию толкают к анемичности (публичные свойства, сеттеры). Это не приговор: можно:
- маппить на закрытые конструкторы и фабрики;
- держать богатую модель в ядре, а persistence — в инфраструктуре (чистая архитектура).
Чек-лист перед merge
- Новый
string/intв публичном API — имя понятия из домена есть? - Логика в
*Service— можно ли перенести на сущность без «божественного» сервиса? - Два метода с пятью примитивами — нужен ли parameter object?
- Тест домена — без мока БД и HTTP?
Указатель тем — 13. Дальше: Изменяемость → Декларативный стиль → Условия и null.
См. также
Другие статьи этого же раздела в боковом меню (как на странице "О разделе"). Именование, форматирование, комментарии, документация в коде и базовые принципы читаемости — практики, которые команда договаривается соблюдать каждый день. Цикломатическая сложность — одна из наиболее устойчиво применяемых метрик статического анализа программного кода, призванная количественно оценивать логическую структуру исполняемого модуля. Правило MAPPER (Model Abstract Partial Programmable Explaining Reality) — как сопоставлять реальность и код один к одному. const, иммутабельность, ленивая инициализация и побочные эффекты в читаемом коде. Разделение «что» и «как», итерации, магические числа, callback hell и явные ошибки. Меньше if и switch, отказ от null, быстрый провал и полиморфизм вместо флагов. Singleton, god object, shotgun surgery, feature envy и глобальное состояние — симптомы и приёмы рефакторинга. Мёртвый код, лишние абстракции, отключённые предупреждения и отложенный рефакторинг. Приватные методы, flaky-тесты, assertTrue, моки и данные — качество тестового кода и связь с разделом тестирования. Пустые catch, исключения как goto, узкие try и сообщения для пользователя. Краткие итоги раздела "Культура кода". Вопросы перед отправкой кода в репозиторий — именование, принципы, тесты, ревью и безопасность. Ответы ищите в статьях раздела.Культура написания и поддержки кода
Цикломатическая сложность и читаемость кода
MAPPER — модель кода и предметная область
Изменяемость, побочные эффекты и неизменяемые данные
Декларативный код — что и как
Условия, null и явные контракты
Связанность, глобалы и запахи модульности
YAGNI, быстрый провал и техдолг в коде
Тесты как часть культуры кода
Исключения и обработка ошибок в читаемом коде
Итоги
Чек-лист самопроверки