Шаг 3: репозиторий в домене
Заказ становится сущностью
Orderс правилом отмены, доступ к заказам - интерфейсомOrderRepository, а SQL уходит в одну из его реализаций.
Урок: Выделение репозитория. Предыдущий шаг: шлюз таблицы.
Что было
SQL собран в OrdersTable, но контроллер получает от него ряды, знает имена колонок и сам решает, можно ли отменить заказ. Тестировать контроллер можно только вместе с базой.
Что меняем
Три перемены, каждая под зелёными тестами.
Сущность. Order с полями id, customerId, totalCents и статусом. Сумма проверяется при создании, отмена - метод cancel(), который бросает OrderCannotBeCancelled (в Go возвращает ErrCannotCancel), если заказ уже не новый. Правило, которое раньше пряталось между SELECT и UPDATE, теперь читается в четыре строки.
Интерфейс в домене. OrderRepository описывает коллекцию заказов на языке задачи: nextId, get, save, forCustomer. В нём нет ни SQL, ни рядов, ни Database. Интерфейс принадлежит домену: его форму диктует то, что нужно контроллеру, а не то, что умеет база.
Две реализации. SqlOrderRepository - бывший шлюз, который теперь переводит ряды в Order в одном месте (toOrder) и обратно в save: есть ряд - UPDATE, нет - INSERT. InMemoryOrderRepository хранит копии заказов в памяти, чтобы изменения без save не протекали в хранилище, как и в настоящей базе.
Контроллер зависит только от интерфейса. Он достаёт заказ, просит его отмениться, сохраняет и переводит исключения домена в HTTP-статусы: 422 и 409.
Как это собирается
Composition root - единственное место, которое знает, что репозиторий SQL: Database, над ней SqlOrderRepository, над ним OrdersController. В приложении это контейнер или main. Тесты собирают контроллер с любой реализацией: каждый сценарий прогоняется дважды, с памятью и с SQL, и ответы обязаны совпасть. Так реализация в памяти не может тихо разойтись с настоящей.
Сценарий целиком с выводом: пример на PHP, он же на Go, Python и TypeScript в панели кода. Вывод тот же, что на первом шаге.
Что стало лучше
- Контроллер не знает ни SQL, ни колонок, ни рядов. Его методы - это сценарий: достать, изменить, сохранить, ответить.
- Правило отмены живёт в
Orderи работает одинаково из контроллера, консольной команды или обработчика очереди. - Контроллер тестируется с
InMemoryOrderRepository: без базы, миграций и контейнеров. - В Go все приведения
anyсобраны вtoOrder. Остальной код работает с типизированнымOrder.
Итог
Тесты не менялись с первого шага: те же пять сценариев, те же ответы, другая только строка сборки. Значит, поведение сохранено, а устройство поменялось полностью.
Цена тоже видна: вместо одного класса четыре (сущность, интерфейс, две реализации), а save теперь делает лишний SELECT, чтобы выбрать между INSERT и UPDATE. В настоящей базе это решает upsert или Unit of Work, который помнит, какие объекты новые. Если правил в домене нет, остановиться на втором шаге честнее.
Подробнее о приёме: Репозиторий.
Запуск: make test T=lessons/002-extract-repository/step-3