@e22m4u/js-service
Version:
Реализация сервис-локатора для JavaScript
228 lines (214 loc) • 6.64 kB
JavaScript
import {SERVICE_CLASS_NAME} from './service.js';
import {InvalidArgumentError} from './errors/index.js';
/**
* Service class name.
*
* @type {string}
*/
export const SERVICE_CONTAINER_CLASS_NAME = 'ServiceContainer';
/**
* Service container.
*/
export class ServiceContainer {
/**
* Kinds.
*
* @type {string[]}
*/
static kinds = [SERVICE_CONTAINER_CLASS_NAME];
/**
* Services map.
*
* @type {Map<any, any>}
* @private
*/
_services = new Map();
/**
* Parent container.
*
* @type {ServiceContainer}
* @private
*/
_parent;
/**
* Constructor.
*
* @param {ServiceContainer|undefined} parent
*/
constructor(parent = undefined) {
if (parent != null) {
if (!(parent instanceof ServiceContainer))
throw new InvalidArgumentError(
'The provided parameter "parent" of ServicesContainer.constructor ' +
'must be an instance ServiceContainer, but %v given.',
parent,
);
this._parent = parent;
}
}
/**
* Получить родительский сервис-контейнер или выбросить ошибку.
*
* @returns {ServiceContainer}
*/
getParent() {
if (!this._parent)
throw new InvalidArgumentError('The service container has no parent.');
return this._parent;
}
/**
* Проверить наличие родительского сервис-контейнера.
*
* @returns {boolean}
*/
hasParent() {
return Boolean(this._parent);
}
/**
* Получить существующий или новый экземпляр.
*
* @param {*} ctor
* @param {*} args
* @returns {*}
*/
get(ctor, ...args) {
if (!ctor || typeof ctor !== 'function')
throw new InvalidArgumentError(
'The first argument of ServicesContainer.get must be ' +
'a class constructor, but %v given.',
ctor,
);
// если конструктор отсутствует в текущем
// контейнере, но имеется в родительском,
// то запрашиваем сервис именно из него
if (!this._services.has(ctor) && this._parent && this._parent.has(ctor)) {
return this._parent.get(ctor);
}
let service = this._services.get(ctor);
// если экземпляр сервиса не найден,
// то пытаемся найти его наследников
if (!service) {
const ctors = this._services.keys();
const inheritedCtor = ctors.find(v => v.prototype instanceof ctor);
if (inheritedCtor) {
service = this._services.get(inheritedCtor);
// если наследник найден, но экземпляр отсутствует,
// то подменяем конструктор наследником, чтобы
// экземпляр создавался с помощью него
ctor = inheritedCtor;
}
}
// если экземпляр сервиса не найден
// или переданы аргументы, то создаем
// новый экземпляр
if (!service || args.length) {
service =
Array.isArray(ctor.kinds) && ctor.kinds.includes(SERVICE_CLASS_NAME)
? new ctor(this, ...args)
: new ctor(...args);
this._services.set(ctor, service);
// instantiates from a factory function
} else if (typeof service === 'function') {
service = service();
this._services.set(ctor, service);
}
return service;
}
/**
* Получить существующий или новый экземпляр,
* только если конструктор зарегистрирован.
*
* @param {*} ctor
* @param {*} args
* @returns {*}
*/
getRegistered(ctor, ...args) {
if (!this.has(ctor))
throw new InvalidArgumentError(
'The constructor %v is not registered.',
ctor,
);
return this.get(ctor, ...args);
}
/**
* Проверить существование конструктора в контейнере.
*
* @param {*} ctor
* @returns {boolean}
*/
has(ctor) {
if (this._services.has(ctor)) return true;
if (this._parent) return this._parent.has(ctor);
// если не удалось найти указанный конструктор,
// то пытаемся найти его наследника
const ctors = this._services.keys();
const inheritedCtor = ctors.find(v => v.prototype instanceof ctor);
if (inheritedCtor) return true;
return false;
}
/**
* Добавить конструктор в контейнер.
*
* @param {*} ctor
* @param {*} args
* @returns {this}
*/
add(ctor, ...args) {
if (!ctor || typeof ctor !== 'function')
throw new InvalidArgumentError(
'The first argument of ServicesContainer.add must be ' +
'a class constructor, but %v given.',
ctor,
);
const factory = () =>
Array.isArray(ctor.kinds) && ctor.kinds.includes(SERVICE_CLASS_NAME)
? new ctor(this, ...args)
: new ctor(...args);
this._services.set(ctor, factory);
return this;
}
/**
* Добавить конструктор и создать экземпляр.
*
* @param {*} ctor
* @param {*} args
* @returns {this}
*/
use(ctor, ...args) {
if (!ctor || typeof ctor !== 'function')
throw new InvalidArgumentError(
'The first argument of ServicesContainer.use must be ' +
'a class constructor, but %v given.',
ctor,
);
const service =
Array.isArray(ctor.kinds) && ctor.kinds.includes(SERVICE_CLASS_NAME)
? new ctor(this, ...args)
: new ctor(...args);
this._services.set(ctor, service);
return this;
}
/**
* Добавить конструктор и связанный экземпляр.
*
* @param {*} ctor
* @param {*} service
* @returns {this}
*/
set(ctor, service) {
if (!ctor || typeof ctor !== 'function')
throw new InvalidArgumentError(
'The first argument of ServicesContainer.set must be ' +
'a class constructor, but %v given.',
ctor,
);
if (!service || typeof service !== 'object' || Array.isArray(service))
throw new InvalidArgumentError(
'The second argument of ServicesContainer.set must be ' +
'an Object, but %v given.',
service,
);
this._services.set(ctor, service);
return this;
}
}