@e22m4u/js-service
v0.6.2
Published
Реализация принципа инверсии управления для JavaScript
Readme
@e22m4u/js-service
Модуль реализует принцип инверсии управления (Inversion of Control), через паттерн Service Locator в связке с DI-контейнером. Встроенные классы данного модуля берут на себя ответственность за создание, хранение и жизненный цикл объектов, освобождая зависимости приложения от жестких связей и ручного вызова конструкторов.
Содержание
- Установка
- Описание
- Базовые примеры
- ServiceContainer
- Иерархия контейнеров
- Service
- DebuggableService
- Тесты
- Лицензия
Установка
npm install @e22m4u/js-serviceМодуль поддерживает ESM и CommonJS стандарты.
ESM
import {Service} from '@e22m4u/js-service';CommonJS
const {Service} = require('@e22m4u/js-service');Описание
Модуль экспортирует два основных класса ServiceContainer и Service,
которые можно использовать как по отдельности, так и вместе для построения
слабосвязанной архитектуры.
ServiceContainer(IoC-контейнер)
Реализация сервис-контейнера для хранения и разрешения зависимостей.Service(базовый класс для наследования сервисами)
Инкапсулирует работу с сервис-контейнером, предоставляя наследуемым от него сервисам простой интерфейс для доступа к зависимостям.
Дополнительно:
DebuggableService(базовый Service + инструменты логирования)
Расширенная версия классаServiceс дополнительным функционалом для логирования.
Базовые примеры
Создание контейнера и экземпляра сервиса по принципу «одиночки».
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Использование сервиса внутри другого как зависимость.
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);Сервис как точка входа приложения.
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();Подмена сервиса в контейнере.
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)получить существующий или новый экземпляр;getRegistered(ctor, ...args)получить существующий или новый экземпляр, только если указанный конструктор зарегистрирован в контейнере, в противном случае выбрасывается ошибка;has(ctor)проверить существование конструктора в контейнере;add(ctor, ...args)добавить конструктор в контейнер (ленивая инициализация);use(ctor, ...args)добавить конструктор и сразу создать экземпляр;set(ctor, service)добавить конструктор и связанный с ним готовый экземпляр;getParent()получить родительский сервис-контейнер;hasParent()проверить наличие родительского сервис-контейнера;
В сигнатурах методов используется вспомогательный тип конструктора:
/**
* Конструктор класса.
*/
interface Constructor<T extends object = object> {
new (...args: any[]): T;
}serviceContainer.get
Метод get класса ServiceContainer создает экземпляр
полученного конструктора и сохраняет его для последующих
обращений по принципу "одиночки" (Singleton).
Сигнатура:
/**
* Получить существующий или новый экземпляр.
*
* @param ctor
* @param args
*/
get<T extends object>(ctor: Constructor<T>, ...args: any[]): T;Пример:
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 может принимать аргументы конструктора. При этом, если контейнер
уже имеет экземпляр данного конструктора, то он будет пересоздан с новыми
аргументами.
Пример:
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:00serviceContainer.getRegistered
Работает аналогично get, но выбрасывает ошибку, если конструктор
сервиса не был предварительно зарегистрирован через add, use или set.
Это обеспечивает более строгий контроль над зависимостями.
Сигнатура:
/**
* Получить существующий или новый экземпляр,
* только если конструктор зарегистрирован.
*
* @param ctor
* @param args
*/
getRegistered<T extends object>(ctor: Constructor<T>, ...args: any[]): T;Пример:
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.
Сигнатура:
/**
* Проверить существование конструктора в контейнере.
*
* @param ctor
*/
has<T extends object>(ctor: Constructor<T>): boolean;Пример:
class MyService {}
const container = new ServiceContainer();
console.log(container.has(MyService)); // false
container.add(MyService);
console.log(container.has(MyService)); // trueserviceContainer.add
Регистрирует конструктор в контейнере, но не создает экземпляр в момент вызова. Экземпляр будет создан только при первом доступе к сервису. Метод позволяет указать аргументы, которые будут использованы для создания экземпляра.
Сигнатура:
/**
* Добавить конструктор в контейнер.
*
* @param ctor
* @param args
*/
add<T extends object>(ctor: Constructor<T>, ...args: any[]): this;Пример:
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
Немедленно создает и кэширует экземпляр сервиса. Может использоваться, когда сервис должен быть проинициализирован сразу при настройке другого компонента.
Сигнатура:
/**
* Добавить конструктор и создать экземпляр.
*
* @param ctor
* @param args
*/
use<T extends object>(ctor: Constructor<T>, ...args: any[]): this;Пример:
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 getserviceContainer.set
Метод позволяет связать конструктор с уже существующим экземпляром. Может быть использован для подмены зависимостей в тестах или для внедрения экземпляров, созданных вне контейнера.
Сигнатура:
/**
* Добавить конструктор и связанный экземпляр.
*
* @param ctor
* @param service
*/
set<T extends object>(ctor: Constructor<T>, service: T): this;Пример:
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); // trueserviceContainer.getParent
Метод возвращает родительский контейнер. Если у текущего контейнера нет родителя, то метод выбрасывает ошибку.
Сигнатура:
/**
* Получить родительский сервис-контейнер или выбросить ошибку.
*/
getParent(): ServiceContainer;Пример:
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, который
выбрасывает ошибку при отсутствии родителя.
Сигнатура:
/**
* Проверить наличие родительского сервис-контейнера.
*/
hasParent(): boolean;Пример:
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 первым параметром принимает родительский
контейнер, который используется как альтернативный, если конструктор
запрашиваемого сервиса не зарегистрирован в текущем.
class MyService {}
// создание контейнера и регистрация сервиса MyService
const parentContainer = new ServiceContainer();
parentContainer.add(MyService);
// использование созданного ранее контейнера в качестве
// родителя, и проверка наличия сервиса MyService
const childContainer = new ServiceContainer(parentContainer);
const hasService = childContainer.has(MyService);
console.log(hasService); // trueService
Методы:
getService(ctor, ...args)получить существующий или новый экземпляр;getRegisteredService(ctor, ...args)получить существующий или новый экземпляр, только если указанный конструктор зарегистрирован в контейнере, в противном случае выбрасывается ошибка;hasService(ctor)проверить существование конструктора в контейнере;addService(ctor, ...args)добавить конструктор в контейнер;useService(ctor, ...args)добавить конструктор и создать экземпляр;setService(ctor, service)добавить конструктор и его экземпляр;
Сервисом может являться совершенно любой класс. Однако, если это
наследник класса Service, то такой сервис позволяет инкапсулировать
создание сервис-контейнера, его хранение и передачу другим сервисам.
Пример:
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 обеспечивает существование единственного экземпляра
запрашиваемого сервиса, и не создает новый экземпляр при повторных
обращениях. Однако, при передаче дополнительных аргументов, сервис
будет переопределен с новыми аргументами конструктора.
Сигнатура:
/**
* Получить существующий или новый экземпляр.
*
* @param ctor
* @param args
*/
getService<T extends object>(ctor: Constructor<T>, ...args: any[]): T;Пример:
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); // trueservice.getRegisteredService
Работает аналогично getService, но выбрасывает ошибку, если конструктор
сервиса не был предварительно зарегистрирован, что обеспечивает более
строгий контроль над зависимостями.
Сигнатура:
/**
* Получить существующий или новый экземпляр,
* только если конструктор зарегистрирован.
*
* @param ctor
* @param args
*/
getRegisteredService<T extends object>(
ctor: Constructor<T>,
...args: any[],
): T;Пример:
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. Полезно для условного запроса зависимостей.
Сигнатура:
/**
* Проверка существования конструктора в контейнере.
*
* @param ctor
*/
hasService<T extends object>(ctor: Constructor<T>): boolean;Пример:
class OptionalLogger {}
class MyService extends Service {
log(message) {
if (this.hasService(OptionalLogger)) {
const logger = this.getService(OptionalLogger);
logger.log(message);
}
}
}service.addService
Регистрирует конструктор в контейнере, но не создает экземпляр в момент вызова. Экземпляр будет создан только при первом доступе к сервису. Метод позволяет указать аргументы, которые будут использованы для создания экземпляра.
Сигнатура:
/**
* Добавить конструктор в контейнер.
*
* @param ctor
* @param args
*/
addService<T extends object>(ctor: Constructor<T>, ...args: any[]): this;Пример:
class DatabaseService {}
class Config {}
class App extends Service {
setupDatabase() {
const config = new Config();
// регистрация сервиса с аргументами для конструктора
this.addService(DatabaseService, config);
}
}service.useService
Немедленно создает и кэширует экземпляр сервиса. Может использоваться, когда сервис должен быть проинициализирован сразу при настройке другого компонента.
Сигнатура:
/**
* Добавить конструктор и создать экземпляр.
*
* @param ctor
* @param args
*/
useService<T extends object>(ctor: Constructor<T>, ...args: any[]): this;Пример:
class Logger {
constructor() {
console.log('Logger is ready.');
}
}
class App extends Service {
init() {
// немедленно создает и кэширует экземпляр Logger
this.useService(Logger); // -> "Logger is ready."
}
}service.setService
Метод позволяет связать конструктор с уже существующим экземпляром. Может быть использован для подмены зависимостей в тестах или для внедрения экземпляров, созданных вне контейнера.
Сигнатура:
/**
* Добавить конструктор и связанный экземпляр.
*
* @param ctor
* @param service
*/
setService<T extends object>(ctor: Constructor<T>, service: T): this;Пример:
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 раздел «Класс Debuggable»)
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: '[email protected]'
// myApp:userService:getUserById:b9c2 }Тесты
npm run testЛицензия
MIT
