@e22m4u/js-service
Version:
Реализация принципа инверсии управления для JavaScript
865 lines (670 loc) • 29.1 kB
Markdown
## @e22m4u/js-service


Модуль реализует принцип инверсии управления (*Inversion of Control*), через
паттерн *Service Locator* в связке с *DI*-контейнером. Встроенные классы данного
модуля берут на себя ответственность за создание, хранение и жизненный цикл
объектов, освобождая зависимости приложения от жестких связей и ручного вызова
конструкторов.
## Содержание
- [Установка](#установка)
- [Описание](#описание)
- [Базовые примеры](#базовые-примеры)
- [ServiceContainer](#servicecontainer)
- [Иерархия контейнеров](#иерархия-контейнеров)
- [Service](#service)
- [DebuggableService](#debuggableservice)
- [Тесты](#тесты)
- [Лицензия](#лицензия)
## Установка
```bash
npm install @e22m4u/js-service
```
Модуль поддерживает ESM и CommonJS стандарты.
*ESM*
```js
import {Service} from '@e22m4u/js-service';
```
*CommonJS*
```js
const {Service} = require('@e22m4u/js-service');
```
## Описание
Модуль экспортирует два основных класса `ServiceContainer` и `Service`,
которые можно использовать как по отдельности, так и вместе для построения
слабосвязанной архитектуры.
- `ServiceContainer` *(IoC-контейнер)*
Реализация сервис-контейнера для хранения и разрешения зависимостей.
- `Service` *(базовый класс для наследования сервисами)*
Инкапсулирует работу с сервис-контейнером, предоставляя наследуемым
от него сервисам простой интерфейс для доступа к зависимостям.
Дополнительно:
- `DebuggableService` *(базовый Service + инструменты логирования)*
Расширенная версия класса `Service` с дополнительным функционалом
для логирования.
## Базовые примеры
Создание контейнера и экземпляра сервиса по принципу *«одиночки»*.
```js
import {ServiceContainer} from '@e22m4u/js-service';
class LoggerService {
log(message) {
console.log(`[LOG]: ${message}`);
}
}
const container = new ServiceContainer();
const logger1 = container.get(LoggerService); // создание и кэширование экземпляра
const logger2 = container.get(LoggerService); // возврат существующего экземпляра
console.log(logger1 === logger2); // true
```
Использование сервиса внутри другого как зависимость.
```js
import {Service} from '@e22m4u/js-service';
import {ServiceContainer} from '@e22m4u/js-service';
// сервис логирования
class LoggerService {
log(message) {
console.log(`[LOG]: ${message}`);
}
}
// сервис калькуляции
class CalculatorService extends Service {
// так как для работы данного сервиса требуется другой сервис,
// выполняется наследование класса Service, чтобы иметь доступ
// к методу getService, через который запрашиваются зависимости
add(a, b) {
const logger = this.getService(LoggerService); // <= зависимость
// при первом обращении к сервису LoggerService создается
// новый экземпляр, который возвращается при повторном доступе
const result = a + b;
logger.log(`${a} + ${b} = ${result}`);
return result;
}
}
// создание экземпляра и вызов метода
const calculator = new CalculatorService();
calculator.add(4, 6);
// [LOG]: 4 + 6 = 10
// альтернативный способ (явное создание контейнера)
// const container = new ServiceContainer();
// const calculator = container.get(CalculatorService);
// calculator.add(4, 6);
```
Сервис как точка входа приложения.
```js
import {Service} from '@e22m4u/js-service';
// сервис логирования
class LoggerService {
log(message) {
console.log(`[LOG]: ${message}`);
}
}
// сервис пользователей
class UserService extends Service { // наследование метода getService
findUserById(id) {
const logger = this.getService(LoggerService); // <= зависимость
logger.log(`Finding user by id ${id}`);
const user = {id, name: 'Jane Doe'};
logger.log(`Found user with name "${user.name}".`);
return user;
}
}
// приложение (точка входа)
class App extends Service { // наследование метода getService
start() {
const logger = this.getService(LoggerService); // <= зависимость
logger.log('Starting App...');
const userService = this.getService(UserService);
const user = userService.findUserById(123);
logger.log('Done.');
}
}
// создание экземпляра из запуск приложения
const app = new App();
app.start();
// альтернативный способ (явное создание контейнера)
// const container = new ServiceContainer();
// const app = container.get(App);
// app.start();
```
Подмена сервиса в контейнере.
```js
import {ApiService} from './api-service';
import {MockApiService} from './mock-api-service';
import {ServiceContainer} from '@e22m4u/js-service';
const container = new ServiceContainer();
// подмена реализации ApiService
container.set(ApiService, new MockApiService());
// любой сервис, который запросит ApiService
// из этого контейнера, получит MockApiService
// MyService зависит от ApiService
const myService = container.get(MyService);
```
## ServiceContainer
В роли IoC-контейнера выступает класс `ServiceContainer`. Он отвечает
за регистрацию, создание и предоставление экземпляров сервисов (зависимостей).
Методы:
- [`get(ctor, ...args)`](#servicecontainerget) получить существующий или новый экземпляр;
- [`getRegistered(ctor, ...args)`](#servicecontainergetregistered) получить существующий или новый
экземпляр, только если указанный конструктор зарегистрирован
в контейнере, в противном случае выбрасывается ошибка;
- [`has(ctor)`](#servicecontainerhas) проверить существование конструктора в контейнере;
- [`add(ctor, ...args)`](#servicecontaineradd) добавить конструктор в контейнер (ленивая инициализация);
- [`use(ctor, ...args)`](#servicecontaineruse) добавить конструктор и сразу создать экземпляр;
- [`set(ctor, service)`](#servicecontainerset) добавить конструктор и связанный с ним готовый экземпляр;
- [`getParent()`](#servicecontainergetparent) получить родительский сервис-контейнер;
- [`hasParent()`](#servicecontainerhasparent) проверить наличие родительского сервис-контейнера;
В сигнатурах методов используется вспомогательный тип конструктора:
```ts
/**
* Конструктор класса.
*/
interface Constructor<T extends object = object> {
new (...args: any[]): T;
}
```
### serviceContainer.get
Метод `get` класса `ServiceContainer` создает экземпляр
полученного конструктора и сохраняет его для последующих
обращений по принципу "одиночки" (Singleton).
Сигнатура:
```ts
/**
* Получить существующий или новый экземпляр.
*
* @param ctor
* @param args
*/
get<T extends object>(ctor: Constructor<T>, ...args: any[]): T;
```
Пример:
```js
import {ServiceContainer} from '@e22m4u/js-service';
// создание контейнера
const container = new ServiceContainer();
// в качестве сервиса используется класс Date (как пример)
const myDate1 = container.get(Date); // создает и кэширует экземпляр
const myDate2 = container.get(Date); // возвращает существующий экземпляр
console.log(myDate1 === myDate2); // true
```
Метод `get` может принимать аргументы конструктора. При этом, если контейнер
уже имеет экземпляр данного конструктора, то он будет пересоздан с новыми
аргументами.
Пример:
```js
const myDate1 = container.get(Date, '2025-01-01'); // создание экземпляра
const myDate2 = container.get(Date); // возврат существующего
const myDate3 = container.get(Date, '2030-05-05'); // пересоздание
console.log(myDate1); // Wed Jan 01 2025 03:00:00
console.log(myDate2); // Wed Jan 01 2025 03:00:00
console.log(myDate3); // Sun May 05 2030 03:00:00
```
### serviceContainer.getRegistered
Работает аналогично `get`, но выбрасывает ошибку, если конструктор
сервиса не был предварительно зарегистрирован через `add`, `use` или `set`.
Это обеспечивает более строгий контроль над зависимостями.
Сигнатура:
```ts
/**
* Получить существующий или новый экземпляр,
* только если конструктор зарегистрирован.
*
* @param ctor
* @param args
*/
getRegistered<T extends object>(ctor: Constructor<T>, ...args: any[]): T;
```
Пример:
```js
class RegisteredService {}
class UnregisteredService {}
const container = new ServiceContainer();
container.add(RegisteredService);
// успешный доступ к зарегистрированному сервису
const service = container.getRegistered(RegisteredService);
// следующий вызов выбросит ошибку,
// так как сервис не зарегистрирован
container.getRegistered(UnregisteredService);
// InvalidArgumentError:
// Constructor UnregisteredService is not registered.
```
### serviceContainer.has
Проверяет, зарегистрирован ли конструктор в контейнере (или в одном
из его родительских контейнеров). Возвращает `true` или `false`.
Сигнатура:
```ts
/**
* Проверить существование конструктора в контейнере.
*
* @param ctor
*/
has<T extends object>(ctor: Constructor<T>): boolean;
```
Пример:
```js
class MyService {}
const container = new ServiceContainer();
console.log(container.has(MyService)); // false
container.add(MyService);
console.log(container.has(MyService)); // true
```
### serviceContainer.add
Регистрирует конструктор в контейнере, но не создает экземпляр в момент
вызова. Экземпляр будет создан только при первом доступе к сервису. Метод
позволяет указать аргументы, которые будут использованы для создания
экземпляра.
Сигнатура:
```ts
/**
* Добавить конструктор в контейнер.
*
* @param ctor
* @param args
*/
add<T extends object>(ctor: Constructor<T>, ...args: any[]): this;
```
Пример:
```js
class MyService {
constructor(name) {
console.log('MyService instantiated!');
console.log(`Hello ${name}!`);
}
}
const container = new ServiceContainer();
console.log('Before add');
container.add(MyService, 'World'); // регистрация, конструктор еще не вызван
console.log('Before get');
const service = container.get(MyService); // создание экземпляра
// Before add
// Before get
// MyService instantiated!
// Hello World!
```
Аргументы, переданные в `add`, будут использованы при создании
экземпляра, если `get` будет вызван без аргументов.
### serviceContainer.use
Немедленно создает и кэширует экземпляр сервиса. Может использоваться, когда
сервис должен быть проинициализирован сразу при настройке другого компонента.
Сигнатура:
```ts
/**
* Добавить конструктор и создать экземпляр.
*
* @param ctor
* @param args
*/
use<T extends object>(ctor: Constructor<T>, ...args: any[]): this;
```
Пример:
```js
class MyService {
constructor(name) {
console.log('MyService instantiated!');
console.log(`Hello ${name}!`);
}
}
const container = new ServiceContainer();
console.log('Before use');
container.use(MyService, 'World'); // создание экземпляра
console.log('Before get');
const service = container.get(MyService); // извлечение экземпляр
// Before use
// MyService instantiated!
// Hello World!
// Before get
```
### serviceContainer.set
Метод позволяет связать конструктор с уже существующим экземпляром.
Может быть использован для подмены зависимостей в тестах или для внедрения
экземпляров, созданных вне контейнера.
Сигнатура:
```ts
/**
* Добавить конструктор и связанный экземпляр.
*
* @param ctor
* @param service
*/
set<T extends object>(ctor: Constructor<T>, service: T): this;
```
Пример:
```js
class ApiService {}
class MockApiService {
// имитация реального ApiService
fetch() {
return 'mock data';
}
}
const container = new ServiceContainer();
const mock = new MockApiService();
// установка экземпляра для ApiService
container.set(ApiService, mock);
const api = container.get(ApiService);
console.log(api.fetch()); // "mock data"
console.log(api === mock); // true
```
### serviceContainer.getParent
Метод возвращает родительский контейнер. Если у текущего контейнера
нет родителя, то метод выбрасывает ошибку.
Сигнатура:
```ts
/**
* Получить родительский сервис-контейнер или выбросить ошибку.
*/
getParent(): ServiceContainer;
```
Пример:
```js
const parentContainer = new ServiceContainer();
const childContainer = new ServiceContainer(parentContainer);
// получение ссылки на родительский контейнер
const parent = childContainer.getParent();
console.log(parent === parentContainer); // true
// попытка получить родителя у корневого
// контейнера вызовет ошибку
try {
parentContainer.getParent();
} catch (error) {
console.log(error.message);
// InvalidArgumentError:
// Service container has no parent.
}
```
### serviceContainer.hasParent
Метод проверяет наличие родительского контейнера и возвращает логическое
значение. Данный метод полезен перед вызовом метода `getParent`, который
выбрасывает ошибку при отсутствии родителя.
Сигнатура:
```ts
/**
* Проверить наличие родительского сервис-контейнера.
*/
hasParent(): boolean;
```
Пример:
```js
const parentContainer = new ServiceContainer();
const childContainer = new ServiceContainer(parentContainer);
console.log(parentContainer.hasParent()); // false
console.log(childContainer.hasParent()); // true
if (childContainer.hasParent()) {
const parent = childContainer.getParent();
// логика работы с родителем
}
```
### Иерархия контейнеров
Конструктор `ServiceContainer` первым параметром принимает родительский
контейнер, который используется как альтернативный, если конструктор
запрашиваемого сервиса не зарегистрирован в текущем.
```js
class MyService {}
// создание контейнера и регистрация сервиса MyService
const parentContainer = new ServiceContainer();
parentContainer.add(MyService);
// использование созданного ранее контейнера в качестве
// родителя, и проверка наличия сервиса MyService
const childContainer = new ServiceContainer(parentContainer);
const hasService = childContainer.has(MyService);
console.log(hasService); // true
```
## Service
Методы:
- [`getService(ctor, ...args)`](#servicegetservice) получить существующий или новый экземпляр;
- [`getRegisteredService(ctor, ...args)`](#servicegetregisteredservice) получить существующий или новый
экземпляр, только если указанный конструктор зарегистрирован
в контейнере, в противном случае выбрасывается ошибка;
- [`hasService(ctor)`](#servicehasservice) проверить существование конструктора в контейнере;
- [`addService(ctor, ...args)`](#serviceaddservice) добавить конструктор в контейнер;
- [`useService(ctor, ...args)`](#serviceuseservice) добавить конструктор и создать экземпляр;
- [`setService(ctor, service)`](#servicesetservice) добавить конструктор и его экземпляр;
Сервисом может являться совершенно любой класс. Однако, если это
наследник класса `Service`, то такой сервис позволяет инкапсулировать
создание сервис-контейнера, его хранение и передачу другим сервисам.
Пример:
```js
import {Service} from '@e22m4u/js-service';
// сервис Foo
class Foo extends Service {
method() {
// доступ к сервису Bar
const bar = this.getService(Bar);
// ...
}
}
// сервис Bar
class Bar extends Service {
method() {
// доступ к сервису Foo
const foo = this.getService(Foo);
// ...
}
}
// сервис App (точка входа)
class App extends Service {
method() {
// доступ к сервисам Foo и Bar
const foo = this.getService(Foo);
const bar = this.getService(Bar);
// ...
}
}
const app = new App();
```
В примере выше мы не заботились о создании контейнера и его передачу
между сервисами, так как эта логика инкапсулирована в базовом
классе `Service`.
### service.getService
Метод `getService` обеспечивает существование единственного экземпляра
запрашиваемого сервиса, и не создает новый экземпляр при повторных
обращениях. Однако, при передаче дополнительных аргументов, сервис
будет переопределен с новыми аргументами конструктора.
Сигнатура:
```ts
/**
* Получить существующий или новый экземпляр.
*
* @param ctor
* @param args
*/
getService<T extends object>(ctor: Constructor<T>, ...args: any[]): T;
```
Пример:
```js
const foo1 = this.getService(Foo, 'arg'); // создание экземпляра
const foo2 = this.getService(Foo); // возврат существующего
console.log(foo1 === foo2); // true
const foo3 = this.getService(Foo, 'arg'); // пересоздание экземпляра
const foo4 = this.getService(Foo); // возврат уже пересозданного
console.log(foo3 === foo4); // true
```
### service.getRegisteredService
Работает аналогично `getService`, но выбрасывает ошибку, если конструктор
сервиса не был предварительно зарегистрирован, что обеспечивает более
строгий контроль над зависимостями.
Сигнатура:
```ts
/**
* Получить существующий или новый экземпляр,
* только если конструктор зарегистрирован.
*
* @param ctor
* @param args
*/
getRegisteredService<T extends object>(
ctor: Constructor<T>,
...args: any[],
): T;
```
Пример:
```js
class RegisteredService {}
class UnregisteredService {}
class MyService extends Service {
run() {
this.addService(RegisteredService);
// успешный доступ к зарегистрированному сервису
const service = this.getRegisteredService(RegisteredService);
// следующий вызов выбросит ошибку,
// так как сервис не зарегистрирован
this.getRegisteredService(UnregisteredService);
// InvalidArgumentError:
// Constructor UnregisteredService is not registered.
}
}
```
### service.hasService
Проверяет, зарегистрирован ли конструктор в контейнере. Возвращает
`true` или `false`. Полезно для условного запроса зависимостей.
Сигнатура:
```ts
/**
* Проверка существования конструктора в контейнере.
*
* @param ctor
*/
hasService<T extends object>(ctor: Constructor<T>): boolean;
```
Пример:
```js
class OptionalLogger {}
class MyService extends Service {
log(message) {
if (this.hasService(OptionalLogger)) {
const logger = this.getService(OptionalLogger);
logger.log(message);
}
}
}
```
### service.addService
Регистрирует конструктор в контейнере, но не создает экземпляр в момент
вызова. Экземпляр будет создан только при первом доступе к сервису. Метод
позволяет указать аргументы, которые будут использованы для создания
экземпляра.
Сигнатура:
```ts
/**
* Добавить конструктор в контейнер.
*
* @param ctor
* @param args
*/
addService<T extends object>(ctor: Constructor<T>, ...args: any[]): this;
```
Пример:
```js
class DatabaseService {}
class Config {}
class App extends Service {
setupDatabase() {
const config = new Config();
// регистрация сервиса с аргументами для конструктора
this.addService(DatabaseService, config);
}
}
```
### service.useService
Немедленно создает и кэширует экземпляр сервиса. Может использоваться, когда
сервис должен быть проинициализирован сразу при настройке другого компонента.
Сигнатура:
```ts
/**
* Добавить конструктор и создать экземпляр.
*
* @param ctor
* @param args
*/
useService<T extends object>(ctor: Constructor<T>, ...args: any[]): this;
```
Пример:
```js
class Logger {
constructor() {
console.log('Logger is ready.');
}
}
class App extends Service {
init() {
// немедленно создает и кэширует экземпляр Logger
this.useService(Logger); // -> "Logger is ready."
}
}
```
### service.setService
Метод позволяет связать конструктор с уже существующим экземпляром.
Может быть использован для подмены зависимостей в тестах или для внедрения
экземпляров, созданных вне контейнера.
Сигнатура:
```ts
/**
* Добавить конструктор и связанный экземпляр.
*
* @param ctor
* @param service
*/
setService<T extends object>(ctor: Constructor<T>, service: T): this;
```
Пример:
```js
class ApiService {}
class MockApiService {}
class MyComponent extends Service {
setupForTest() {
// подмена реального ApiService на его мок-версию
this.setService(ApiService, new MockApiService());
}
fetchData() {
// следующий вызов вернет экземпляр MockApiService
const api = this.getService(ApiService);
return api.fetch();
}
}
```
## DebuggableService
Данный сервис наследует класс `Debuggable` и использует композицию
для получения функциональности класса `Service`.
*(см. подробнее [@e22m4u/js-debug](https://www.npmjs.com/package/@e22m4u/js-debug#класс-debuggable#класс-debuggable) раздел «Класс Debuggable»)*
```js
import {apiClient} from './path/to/apiClient';
import {DebuggableService} from '@e22m4u/js-service';
// определение глобального префикса (область имен)
// для отладочных сообщений текущего приложения
process.env['DEBUGGER_NAMESPACE'] = 'myApp';
// переменная DEBUG обычно устанавливается перед
// запуском Node.js процесса, и указывает на область
// имен для логирования, пример: DEBUG=myApp* node .
process.env['DEBUG'] = 'myApp*';
class UserService extends DebuggableService {
async getUserById(userId) {
// получение отладчика для данного метода
// (для каждого вызова генерируется хэш)
const debug = this.getDebuggerFor(this.getUserById);
debug('Fetching user with ID %v...', userId);
try {
const user = await apiClient.get(`/users/${userId}`);
debug.inspect('User data received:', user);
return user;
} catch (error) {
debug('Failed to fetch user. Error: %s', error.message);
throw error;
}
}
}
const userService = new UserService();
await userService.getUserById(123);
// myApp:userService:constructor:a4f1 Instantiated.
// myApp:userService:getUserById:b9c2 Fetching user with ID 123...
// myApp:userService:getUserById:b9c2 User data received:
// myApp:userService:getUserById:b9c2 {
// myApp:userService:getUserById:b9c2 id: 123,
// myApp:userService:getUserById:b9c2 name: 'John Doe',
// myApp:userService:getUserById:b9c2 email: 'john.doe@example.com'
// myApp:userService:getUserById:b9c2 }
```
## Тесты
```bash
npm run test
```
## Лицензия
MIT