Перейти к основному содержимому

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.

Полный пример

Зависимости по ключу

В качестве зависимости можно передавать не только экземпляры класса, в целом можно передать любое значение, зарегистрировав его по ключу.

Например, как правило у приложений есть некоторый контекст, который зависит от окружения, например содержит в какой домен бекенда ходить в тестинге, а в какой в продакшене.

В простейшем случае это может выглядеть так

// используем специальный тип DependencyKey, который содержит мета-информацию о зависимости
const APP_CONTEXT_KEY = 'APP_CONTEXT' as DependencyKey<AppContext>;

type AppContext = {
env: string;
}

const appContext: AppContext = {
env: "testing"
}
class API extends Dependency {
#host: string;

constructor(@inject(APP_CONTEXT_KEY) {env}: AppContext) {
this.#host = env === "testing" ? "..." : "..."
}
}

Мы можем сделать его доступным для нашего приложения, зарегистрировав его как зависимость по ключу.

function App({children}) {
const di = useDI();

di.registerDependency(APP_CONTEXT_KEY, appContext);

const api = di.createService(API);
di.registerService(api);

di.unregisterService(UserService)
const userService = di.createService(UserService);
di.registerService(userService)

di.unregisterService(OrderService)
const orderService = di.createService(OrderService);
di.registerService(orderService)

return (...);
}

В UI также можно читать значения любых зависимостей

const di = useDI();
const appContext = di.getDependency(APP_CONTEXT_KEY);

Добавим вывод окружения в интерфейсе