Skip to content

Repository files navigation

@e22m4u/js-service

npm version license

Модуль реализует принцип инверсии управления (Inversion of Control), через паттерн Service Locator в связке с DI-контейнером. Встроенные классы данного модуля берут на себя ответственность за создание, хранение и жизненный цикл объектов, освобождая зависимости приложения от жестких связей и ручного вызова конструкторов.

Содержание

Установка

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:00

serviceContainer.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)); // true

serviceContainer.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 get

serviceContainer.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); // true

serviceContainer.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); // true

Service

Методы:

  • 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);               // true

service.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: 'john.doe@example.com'
// myApp:userService:getUserById:b9c2   }

Тесты

npm run test

Лицензия

MIT

About

Реализация принципа инверсии управления для JavaScript

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages