Примитивы, value objects и маленькие типы
Примитивная одержимость (Primitive Obsession) — запах, когда вместо понятий предметной области в API повсюду string, int, double, bool. Каталог Фаулера и практика доменного моделирования предлагают реификацию: ввести маленький тип с правилами. Теория MAPPER — в культуре кода / 5; анемичные оболочки вокруг полей — 7.10 / 6.
Симптомы
SendEmail(string to, string subject, …)— легко перепутать дваstring.decimal amount+string currencyотдельно — валюта «теряется» при сложении.Dictionary<string, object>вместо модели разрешений.latitude: double, longitude: doubleбез единиц и валидации диапазона.- Даты как
stringв трёх форматах в разных слоях.
Каждый такой случай — кандидат в value object (объект-значение): маленький неизменяемый тип, сравниваемый по значению, без собственной идентичности в домене.
Value object vs сущность
| Value object | Сущность (Entity) | |
|---|---|---|
| Идентичность | По полям (Email = тот же адрес) | По id (Order#42) |
| Жизненный цикл | Заменяется целиком | Меняется состояние во времени |
| Пример | Money, Email, DateRange | Customer, Order |
В DDD value objects часто не имеют отдельной строки в БД — они встраиваются в колонки или JSON, но в коде остаются типами.
Реификация по шагам
- Заметить кластер — одни и те же примитивы идут вместе (сумма + валюта, lat + lon).
- Ввести тип с инвариантами в конструкторе или фабрике
Email.parse(raw). - Заменить сигнатуры — IDE «Change signature» + тесты.
- Перенести поведение —
money.add(other)вместоif (currency != other.currency).
public readonly record struct Money(decimal Amount, string Currency) {
public Money Add(Money other) {
if (Currency != other.Currency)
throw new InvalidOperationException("Currency mismatch");
return new Money(Amount + other.Amount, Currency);
}
}
@dataclass(frozen=True)
class Email:
value: str
def __post_init__(self):
if "@" not in self.value:
raise ValueError("invalid email")
Типичные приёмы
| Направление | Суть |
|---|---|
| Маленькие объекты | Вместо «мешков» полей |
| Реификация | Примитив → тип с инвариантами |
| Коллекции | Ассоциативные массивы → типизированные структуры |
| Строки | Не универсальный контейнер для всего |
| Даты | Instant / ZonedDateTime, не «дата строкой» |
| Интервалы | Диапазоны как объекты с правилами |
| Валидация | Email, ISBN и т.п. внутри типа |
Указатель по симптомам — 7.10 / 13.
Приёмы Фаулера
Связка с методами рефакторинга:
- Replace Data Value with Object
- Replace Type Code with Class / Subclasses
- Introduce Parameter Object — когда много примитивов в одном вызове
- Extract Class — когда группа полей «ездит» вместе
Parameter Object
Пять параметров createUser(name, email, phone, country, zip) → createUser(NewUserRequest req) или createUser(ContactInfo contact, Address address). Это промежуточный шаг перед полноценными value objects.
ORM и транспорт
- В БД колонка
email VARCHAR— нормально; в домене —Email. - На границе HTTP — DTO с
string; адаптер сразу строитEmailи дальше только типы. - Не дублировать валидацию в контроллере, сервисе и репозитории — один тип на границе.
Когда примитив уместен
- Счётчик цикла
i, флаги низкоуровневого API. - Внутренние индексы, не уходящие в доменный контракт.
- Прототип и throwaway-скрипт — осознанно, с пометкой в задаче на рефакторинг.
Не оборачивайте каждый int в UserId в утилитарном скрипте на 40 строк. Реифицируйте, когда тип пересекает границы модулей или несёт инвариант, который уже дважды продублирован в коде.
Чек-лист
- Два
stringподряд в публичном методе — можно ли перепутать? → отдельные типы. - Валидация email/суммы/даты копируется — вынести в value object.
Mapс магическими ключами — заменить классом или record.- Тест:
new Email("bad")падает в одном месте.
Дальше: рефакторинг · декларативный стиль · условия и null.
См. также
Другие статьи этого же раздела в боковом меню (как на странице "О разделе"). Что такое код и как он работает - от представления инструкций до компиляции, выполнения и взаимодействия с системой. Теория представления кода - как информация кодируется, передается и искажается в реальных каналах хранения и связи. Ключевые слова (keywords) — это зарезервированные слова в языке программирования, которые имеют специальное значение и не могут использоваться как имена переменных или функций. Что такое операторы, какими они бывают. Действия над данными. Если после имени следует пара круглых скобок — это вызов функции. Если скобок нет — это обращение к значению (переменной, константе, параметру и т. д.). Отсутствие значения — это фундаментальная концепция в программировании, отражающая состояние, когда данные ожидаются, но в текущий момент недоступны. Циклы в программировании - виды повторений, условия завершения и типичные сценарии применения. Высокоуровневые и низкоуровневые языки — абстракция, переносимость, ассемблер и компромисс с производительностью. Синтаксический сахар - конструкции языка, повышающие читаемость кода без изменения вычислительной сути. Единые правила написания. Что такое стиль кода, и как его оформляют. Что делают с кодом, чтобы повысить его надёжность, читаемость и расширяемость. Что такое рефакторинг, как он работает и из чего состоит.Что такое код и как он работает
Теория представления кода
Ключевые слова в языках программирования
Операторы
Функции
Обработка значения null
Циклы
Уровни абстракции языков программирования
Синтаксический сахар
Стили оформления кода
Приёмы написания кода
Методы рефакторинга программного кода