Dependency injection
Сущности в доменной области, как правило, имеют взаимосвязи, и для того чтобы описать какой-либо пользовательский сценарий, требуется оперировать несколькими сущностями, для решения этой задачи во фреймворке есть встроенный DI контейнер, который позволяет декларативно описывать и резолвить зависимости между сервисами.
Зависимости от сервисов
В нашем приложении уже есть сущности пользователя и заказов, давайте представим, что при покупке пользователю начисляются бонусы, и мы хотим после покупки обновить информацию о его текущем количестве бонусов.
Добавим в сервис пользователя соответствующий метод
class UserService extends Service {
@operation
*getBonuses() {
return yield* call(fetchBonuses);
}
}
Теперь нам нужно обновить информацию о бонусах после свершения заказа, но эта л огика находится в разных сервисах, давайте укажем, что OrderService зависит от UserService. Для этого воспользуемся декоратором inject.
class OrderService extends Service {
#userService: UserService
constructor(
@inject(OperationService) operationService: OperationService,
@inject(UserService) userService: UserService
) {
// обязательная инициализация базового класса
super(operationService)
this.#userService = userService;
}
@daemon()
@operation({
id: ORDERS_OPERATION_ID,
updateStrategy: appendStrategy
})
*addOrder() {
const id = getNewId();
const order = {id, description: `Order ${id}`};
yield* call(addOrder, order);
// обновляем информацию о бонусах
yield* call(this.#userService.getBonuses);
return [order];
}
}
Теперь давайте доработаем интерфейс
Зависимости нужно регистрировать в том порядке в котором их надо резолвить.
В нашем примере зависимость UserService => OrderService, в этом порядке их и регистрируем.
Зависимости от пользовательских классов
Кроме сервиса можно инъектировать любой класс, унаследовав его от базового класса Dependency.
Давайте рассмотрим пример, когда это может быть полезно. В нашем приложении нам необходимо делать запросы к бекенду, сейчас это описано набором утилит fetchUser, fetchOrders и т.д. Такой подход не предоставляет никакого явного контракта, работу с бэкендом сложнее тестировать и настраивать — давайте это исправ им.
Создадим для АПИ явный контракт в виде класса.
class API extends Dependency {
toString() {
return 'API'
}
fetchUser() {};
fetchBonuses();
fetchOrders() {};
addOrder(order) {};
}
Зарегистрируем наш класс
function App({children}) {
const di = useDI();
di.registerService(new API())
const userService = di.createService(UserService);
di.registerService(userService)
const orderService = di.createService(OrderService);
di.registerService(orderService)
return (...)
}
Тогда мы можем создать явную зависимость сервиса от API, это может выглядеть как на примере ниже
class UserService extends Service {
#api: API;
toString() {
return "UserService"
}
constructor(
@inject(OperationService) operationService: OperationService,
@inject(API) api: API
) {
super(operationService);
this.#api = api;
}
@operation
*getUserInfo() {
return yield* call(this.#api.fetchUser);
}
@operation
*getBonuses() {
return yield* call(this.#api.fetchBonuses);
}
}
Теперь у нашего API есть явный контракт, а наши сервисы гораздо проще тестировать, т. к. мы можем подставлять любую реализацию контракта для API.
Полный пример