Skip to content

Commit

Permalink
Merge pull request #48 from lad-tech/43-DI-container
Browse files Browse the repository at this point in the history
feat: implement DI container and inject decorator
  • Loading branch information
gleip authored May 24, 2023
2 parents ec80ed8 + aa65b34 commit abd089c
Show file tree
Hide file tree
Showing 27 changed files with 599 additions and 71 deletions.
51 changes: 38 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,18 @@
# nsc-toolkit

Содержание
1. [О библиотеке](#о-библиотеке)
2. [Возможности](#возможности)
3. [Установка](#установка)
4. [Быстрый старт](#быстрый-старт)
5. [Основные компоненты библиотеки](#основные-компоненты-библиотеки)
6. [Рекомендации](#рекомендации)
7. [Пример использования](#пример-использования)
8. [Сворачивание сервисов в монолитное приложение](#сворачивание-сервисов-в-монолитное-приложение)
- [nsc-toolkit](#nsc-toolkit)
- [О библиотеке](#о-библиотеке)
- [Возможности](#возможности)
- [Установка](#установка)
- [Быстрый старт](#быстрый-старт)
- [Переменные окружения](#переменные-окружения)
- [Основные компоненты библиотеки](#основные-компоненты-библиотеки)
- [Рекомендации](#рекомендации)
- [Пример использования](#пример-использования)
- [Описание каталога `examples`](#описание-каталога-examples)
- [Сворачивание сервисов в монолитное приложение](#сворачивание-сервисов-в-монолитное-приложение)
- [Инверсия зависимостей и DI-контейнер](#инверсия-зависимостей-и-di-контейнер)

## О библиотеке

Expand Down Expand Up @@ -269,7 +273,20 @@ npm i

- `getListener` 一 метод получения объекта `EventEmitter` для подписки на события сервиса.

4. Декораторы. Применение декораторов:
4. Класс `Container`. Реализует DI-контейнре. Сам класс не доступен для импорта, а доступен только его экземпляр, что позволяет реализовать шаблон Singlton. В микросервисном варианте контейнер один на сервис. В монолитном варианте использования контейнер один на все приложение. За счет использользования объектов Symbol в качестве ключей для привязки зависимостей исключены коллизии при привязки зависимостей в разных частях приложения через один контейнре. Для привязки зависимости к ключу необходимо указать тип зависимости. Это нужно чтобы библиотека могла корректно создать ээкземпляр зависимости. Всего существуют 3 вида зависимостей:

- `service` 一 сервис как зависимости.
- `adapter` 一 класс с набором асинхронных методов. Например репозиторий или фасад от стороннего API.
- `constant`一 обычный объект. Например объект с конфигурацией.

Публичные методы:

- `bind` 一 привязать реализацию к ключу.
- `unbind` 一 отвязать реализацию от ключа.
- `get` 一 получить реализацию по ключу.
- `getInstance` 一 получить экземпляр реализации. Для зависимости с типом `service` нельзя получить экземпляр через этот метод поскольку для создания экземпляра сервиса требуется контекст в рамках которого он будет работать. Для зависимости с типом `constant` вернется привязанный объект.

5. Декораторы. Применение декораторов:
* `@service`:
- Если в методе сервиса вызывается метод другого сервиса, то клиент сервиса зависимости следует подключить через декоратор `@service`, а к самому классу метода применить декоратор `@related`. Тогда при вызове метода сервиса не пропадет контекст запроса и можно будет использовать распределенные трассировки.
- Инъекция через декоратор `@service` позволяет использовать функциональность сборки приложения в монолит.
Expand Down Expand Up @@ -325,8 +342,7 @@ Service/
├── interfaces.ts
├── service.ts
├── start.ts
├── inversify.config.ts
├── inversify.types.ts
├── inversion.types
├── service.schema.json
├── package.json
├── package-lock.json
Expand Down Expand Up @@ -355,8 +371,7 @@ Service/
- `interfaces.ts` 一 интерфейсы сервиса (генерируется автоматически);
- `service.ts` 一 реализация сервиса (генерируется автоматически);
- `start.ts` 一 точка входа для запуска сервиса (генерируется автоматически);
- `inversify.config.ts` 一 DI-контейнер (файл необязательный, однако рекомендуется реализовывать DI-контейнеры через библиотеку `inversify`, настройки которой хранятся в этом файле);
- `inversify.types.ts` 一 DI-контейнер (файл необязательный, однако рекомендуется реализовывать DI-контейнеры через библиотеку `inversify`, настройки которой хранятся в этом файле);
- `inversion.types.ts` 一 Типы зависимостей используемые в логике сервиса. Типы используются для получения зависимости. Реализация требуемой зависимости привязывается к контейнеру через тип в файле сервиса. [Описание встроенных возможностей инверсии зависимостей](#инверсия-зависимостей-и-di-контейнер).
- `service.schema.json` 一 описание сервиса.

Вся бизнес-логика сконцентрирована в двух местах структуры:
Expand Down Expand Up @@ -430,4 +445,14 @@ Service/
┌──┴───┐ ┌─────┴─────┐ ┌─────┴─────┐
│ Math │ │ Service_2 │ │ Service_3 │
└──────┘ └───────────┘ └───────────┘
```

## Инверсия зависимостей и DI-контейнер

Библиотека реализует возможности по инверсии зависимостей через DI-контейнер. Экземпляр контейнера можно получить импортировав его из библиотеки. Для описания существующих ключей зависимостей рекомендуется использовать отдеьный файл `inversion.types.ts` в корне сервиса. [Пример файла](./examples/LogicService/inversion.types.ts). Для внедрения зависимостей используются свойства класса метода или параметры конструктора. Для описания зависимости используется декоратор `inject`, который можно испортировать из библиотеки. В декоратор необходимо передать символьный ключ из файла `inversion.types.ts`. Саму привязку реализаций к DI-контейнеру рекомендуется осуществлять в основном файле сервиса `service.ts`. Пример с глубоковложенными зависимостями разных типов можно [посмотреть в методе](./examples/LogicService/methods/GetUser.ts). Цепочка внедряемых зависимостей.

```
┌---------┐ ┌───────────-┐ ┌───────────---┐ ┌---------┐
| GetUser |--->│ Repository |--->| Configurator |--->| Storage |
└---------┘ └─────-─────-┘ └─────-─────---┘ └---------┘
```
8 changes: 6 additions & 2 deletions examples/HttpGate/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { NatsConnection } from 'nats';
import LogicService, { WeirdSumRequest } from '../LogicService';
import LogicService, { WeirdSumRequest, GetUserRequest } from '../LogicService';
import MathService from '../MathService';
import { Service } from '../../src/Service';
import { SimpleCache } from '../SimpleCache';
Expand Down Expand Up @@ -29,7 +29,7 @@ const upHttpGate = async (service: Service) => {

mathEmmiter.on('Notify', message => {
logger.info('Get new event "Notify": ', message.data);
})
});

const fastify = Fastify();

Expand All @@ -50,6 +50,10 @@ const upHttpGate = async (service: Service) => {
return await service.buildService(LogicService, request.baggage).weirdSum(request.body);
});

fastify.get<{ Params: GetUserRequest }>('/logic/user/:userId', async request => {
return await service.buildService(LogicService, request.baggage).getUser(request.params);
});

await fastify.listen({ port: HTTP_SERVICE_PORT });
};

Expand Down
15 changes: 15 additions & 0 deletions examples/LogicService/adapters/Configurator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { inject } from '../../../src';
import { TYPES } from '../inversion.types';
import { StoragePort } from '../domain/ports';

export class Configurator {
@inject(TYPES.Storage) private storage: StoragePort;

public async setUserId(userId: string) {
this.storage[userId] = true;
}

public async userIdExist(userId: string) {
return !!this.storage[userId];
}
}
17 changes: 17 additions & 0 deletions examples/LogicService/adapters/Repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { inject } from '../../../src';
import { TYPES } from '../inversion.types';
import { ConfiguratorPort } from '../domain/ports';

export class Repository {
@inject(TYPES.Configurator) private configurator: ConfiguratorPort;

public async getUserById(userId: string) {
const exist = await this.configurator.userIdExist(userId);

if (exist) {
return { firstName: 'Jon', lastName: 'Dow' };
}

return null;
}
}
2 changes: 2 additions & 0 deletions examples/LogicService/adapters/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './Configurator';
export * from './Repository';
4 changes: 4 additions & 0 deletions examples/LogicService/domain/ports/Configurator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface ConfiguratorPort {
setUserId(userId: string): Promise<void>;
userIdExist(userId: string): Promise<boolean>;
}
7 changes: 7 additions & 0 deletions examples/LogicService/domain/ports/Math.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { Readable } from 'stream';

export interface MathPort {
sum(params: { a: number; b: number }): Promise<{ result: number }>;
sumStream(params: Readable): Promise<{ result: number }>;
fibonacci(params: { length: number }): Promise<Readable>;
}
3 changes: 3 additions & 0 deletions examples/LogicService/domain/ports/Repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface RepositoryPort {
getUserById(userId: string): Promise<{ firstName: string; lastName: string } | null>;
}
1 change: 1 addition & 0 deletions examples/LogicService/domain/ports/Storage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type StoragePort = Record<string, boolean>;
4 changes: 4 additions & 0 deletions examples/LogicService/domain/ports/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './Configurator';
export * from './Math';
export * from './Repository';
export * from './Storage';
17 changes: 16 additions & 1 deletion examples/LogicService/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { Client } from '../../src/Client';
import { NatsConnection } from 'nats';
import { WeirdSumRequest, WeirdSumResponse } from './interfaces';
import {
WeirdSumRequest,
WeirdSumResponse,
GetUserRequest,
GetUserResponse,
GetUserRequestV2,
GetUserResponseV2,
} from './interfaces';
import { Baggage, CacheSettings } from '../../src/interfaces';
import { name, methods } from './service.schema.json';
export * from './interfaces';
Expand All @@ -13,4 +20,12 @@ export default class ServiceMathClient extends Client {
public async weirdSum(payload: WeirdSumRequest) {
return this.request<WeirdSumResponse>(`${name}.${methods.WeirdSum.action}`, payload, methods.WeirdSum);
}

public async getUser(payload: GetUserRequest) {
return this.request<GetUserResponse>(`${name}.${methods.GetUser.action}`, payload, methods.GetUser);
}

public async getUserV2(payload: GetUserRequestV2) {
return this.request<GetUserResponseV2>(`${name}.${methods.GetUserV2.action}`, payload, methods.GetUserV2);
}
}
18 changes: 18 additions & 0 deletions examples/LogicService/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,21 @@ export type WeirdSumRequest = {
export type WeirdSumResponse = {
result: number;
};

export type GetUserRequest = {
userId: string;
};

export type GetUserResponse = {
firstName: string;
lastName: string;
};

export type GetUserRequestV2 = {
userId: string;
};

export type GetUserResponseV2 = {
firstName: string;
lastName: string;
};
6 changes: 6 additions & 0 deletions examples/LogicService/inversion.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export const TYPES = {
Math: Symbol.for('Math'),
Repository: Symbol('Repository'),
Configurator: Symbol('Configurator'),
Storage: Symbol('Storage'),
};
22 changes: 22 additions & 0 deletions examples/LogicService/methods/GetUser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { GetUserRequest, GetUserResponse } from '../interfaces';
import { inject } from '../../../src/injector';
import { methods } from '../service.schema.json';
import { TYPES } from '../inversion.types';
import { RepositoryPort } from '../domain/ports';

import { BaseMethod } from '../../../src/Method';

export class GetUser extends BaseMethod {
static settings = methods.GetUser;

@inject(TYPES.Repository) private repository: RepositoryPort;

public async handler({ userId }: GetUserRequest): Promise<GetUserResponse> {
const result = await this.repository.getUserById(userId);
if (!result) {
throw new Error(`User ${userId} not found!`);
}

return result;
}
}
24 changes: 24 additions & 0 deletions examples/LogicService/methods/GetUserV2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { GetUserRequest, GetUserResponse } from '../interfaces';
import { inject } from '../../../src/injector';
import { methods } from '../service.schema.json';
import { TYPES } from '../inversion.types';
import { RepositoryPort } from '../domain/ports';

import { BaseMethod } from '../../../src/Method';

export class GetUserV2 extends BaseMethod {
static settings = methods.GetUserV2;

constructor(@inject(TYPES.Repository) private repository: RepositoryPort) {
super();
}

public async handler({ userId }: GetUserRequest): Promise<GetUserResponse> {
const result = await this.repository.getUserById(userId);
if (!result) {
throw new Error(`User ${userId} not found!`);
}

return result;
}
}
8 changes: 4 additions & 4 deletions examples/LogicService/methods/WeirdSum.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { WeirdSumRequest, WeirdSumResponse } from '../interfaces';
import { related, service } from '../../../src/injector';
import { inject } from '../../../src/injector';
import { methods } from '../service.schema.json';
import { TYPES } from '../inversion.types';
import { MathPort } from '../domain/ports/Math';

import Math from '../../MathService/index';
import { BaseMethod } from '../../../src/Method';

@related
export class WeirdSum extends BaseMethod {
static settings = methods.WeirdSum;
@service(Math) private math: Math;
@inject(TYPES.Math) private math: MathPort;

public async handler(request: WeirdSumRequest): Promise<WeirdSumResponse> {
this.logger.info('sum started: ', request);
Expand Down
42 changes: 40 additions & 2 deletions examples/LogicService/service.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"type": "object",
"properties": {
"a": { "type": "number" },
"b": {"type": "number" }
"b": { "type": "number" }
},
"required": ["a", "b"]
},
Expand All @@ -26,6 +26,44 @@
},
"required": ["result"]
}
},
"GetUser": {
"action": "getuser",
"description": "Get user object",
"request": {
"type": "object",
"properties": {
"userId": { "type": "string" }
},
"required": ["a", "b"]
},
"response": {
"type": "object",
"properties": {
"firstName": { "type": "string" },
"lastName": { "type": "string" }
},
"required": ["result"]
}
},
"GetUserV2": {
"action": "getuserv2",
"description": "Get user object",
"request": {
"type": "object",
"properties": {
"userId": { "type": "string" }
},
"required": ["a", "b"]
},
"response": {
"type": "object",
"properties": {
"firstName": { "type": "string" },
"lastName": { "type": "string" }
},
"required": ["result"]
}
}
}
}
}
25 changes: 23 additions & 2 deletions examples/LogicService/service.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,36 @@
import { Service } from '../../src/Service';
import { Service, DependencyType, container } from '../../src';
import { connect, NatsConnection } from 'nats';
import { name } from './service.schema.json';
import { TYPES } from './inversion.types';

// Ports
import { MathPort, RepositoryPort, ConfiguratorPort, StoragePort } from './domain/ports';

// Adapters
import { Configurator, Repository } from './adapters';

// Services
import Math from '../MathService/index';

// Methods
import { WeirdSum } from './methods/WeirdSum';
import { GetUser } from './methods/GetUser';
import { GetUserV2 } from './methods/GetUserV2';

export const service = async (broker?: NatsConnection) => {
const brokerConnection = broker || (await connect({ servers: ['localhost:4222'] }));

const storage = { test: true };

container.bind<MathPort>(TYPES.Math, DependencyType.SERVICE, Math);
container.bind<RepositoryPort>(TYPES.Repository, DependencyType.ADAPTER, Repository);
container.bind<ConfiguratorPort>(TYPES.Configurator, DependencyType.ADAPTER, Configurator);
container.bind<StoragePort>(TYPES.Storage, DependencyType.CONSTANT, storage);

const service = new Service({
name,
brokerConnection,
methods: [WeirdSum],
methods: [WeirdSum, GetUser, GetUserV2],
});
await service.start();
return service;
Expand Down
Loading

0 comments on commit abd089c

Please sign in to comment.