Start proper IoC refactor

singleton
garrettmills 4 years ago
parent c7c04b5b5f
commit 780f2534ef
No known key found for this signature in database
GPG Key ID: 6ACD58D6ADACFC6E

@ -2,21 +2,35 @@
* @module flitter-di/src/Container * @module flitter-di/src/Container
*/ */
const MissingContainerDefinitionError = require('./MissingContainerDefinitionError')
/** Manages service definitions, instances, and deferred injection. */ /** Manages service definitions, instances, and deferred injection. */
class Container { class Container {
static TYPE_INJECTABLE = Symbol('injectable')
static TYPE_SINGLETON = Symbol('singleton')
/** /**
* Instantiates the container. * Instantiates the container.
* @param {object} definitions - mapping of service name to static service CLASS definition * @param {object} definitions - mapping of service name to static service CLASS definition
*/ */
constructor(definitions = {}) { constructor(definitions = {}) {
const def_map = {}
for ( const def_name in definitions ) {
if ( !definitions.hasOwnProperty(def_name) ) continue
def_map[def_name] = {
type: this.constructor.TYPE_INJECTABLE,
ref: definitions[def_name]
}
}
/** /**
* Static service definitions from which instances are created when * Static IoC item definitions from which instances are created or
* the services are requested. Should be in service name -> service * singleton values are returned when the items are requested.
* Should be mapping of item_name -> {type: Symbol, ref: *}.
* definition pairs. * definition pairs.
* @type {object} * @type {object}
*/ */
this.definitions = definitions this.definitions = def_map
/** /**
* Instantiated services. If a service has already been requested, it is * Instantiated services. If a service has already been requested, it is
@ -59,79 +73,112 @@ class Container {
} }
/** /**
* Fetch a service by name. If no name is provided, return the service * Get the container proxy. Allows accessing IoC items by name.
* proxy container. This container has getters for all the services by * @returns {{}}
* name.
* @param {string} service - the name of the service
* @returns {module:flitter-di/src/Service~Service|undefined} - the service instance or service container proxy
*/ */
service(service = false) { proxy() {
if ( service === false ) return this.proxy() return new Proxy({}, {
get: (what, name) => {
return this.get(name)
}
})
}
if ( !this.definitions[service] ) { /**
throw new Error('No such service registered with this container: '+service) * Register a service class with the container. Allows the
* service to be requested and it will be instantiated and
* injected by the container.
* @param {string} service_name
* @param {typeof module:flitter-di/src/Service~Service} service_class - the uninstantiated Service class
*/
register_service(service_name, service_class) {
this.definitions[service_name] = {
type: this.constructor.TYPE_INJECTABLE,
ref: service_class
} }
// Store the static reference first. // check and process deferrals
// This allows us to resolve circular dependencies. if ( this.deferred_classes.length > 0 ) {
if ( !this.statics[service] ) { const deferred_requests = this.deferred_classes.map(x => x._di_deferred_services).flat()
this.statics[service] = this.definitions[service] if ( deferred_requests.includes(service_name) ) {
if ( this.di ) { this._process_deferral(service_name, this.get(service_name))
this.di.make(this.statics[service])
} }
} }
}
if ( !this.instances[service] ) { /**
const ServiceClass = this.statics[service] * Register an item as a singleton with the container.
this.instances[service] = new ServiceClass() * @param {string} singleton_name
* @param {*} value - the value tobe returned by the container
*/
register_singleton(singleton_name, value) {
this.definitions[singleton_name] = {
type: this.constructor.TYPE_SINGLETON,
ref: value,
} }
return this.instances[service] // check and process deferrals
} if ( this.deferred_classes.length > 0 ) {
const deferred_requests = this.deferred_classes.map(x => x._di_deferred_services).flat()
proxy() { if ( deferred_requests.includes(singleton_name) ) {
return new Proxy({}, { this._process_deferral(singleton_name, this.get(singleton_name))
get: (what, name) => {
return this.service(name)
} }
}) }
} }
/** /**
* Register a class definition as a service. When requested, the service * Fetch a container item by name. It it is an injectable item,
* for this class will be created from the class' instance. * it will be injected and instantiated before return.
* @param {string} service_name - the referential name of the service * @param {string} name - the name of the IoC item
* @param {*} service_class - the service class definition * @returns {module:flitter-di/src/Service~Service|*} - the service instance or singleton item
*/ */
register(service_name, service_class) { get(name) {
this.definitions[service_name] = service_class const def = this.definitions[name]
this._process_deferral(service_name, this.service(service_name)) if ( !def ) throw new MissingContainerDefinitionError(name)
// Return the singleton value, if applicable
if ( def.type === this.constructor.TYPE_SINGLETON ) {
return def.ref
} else if ( def.type === this.constructor.TYPE_INJECTABLE ) {
// Store the static reference first.
// This allows us to resolve circular dependencies.
if ( !this.statics[name] ) {
this.statics[name] = def.ref
if ( this.di ) {
this.di.make(this.statics[name])
}
}
if ( !this.instances[name] ) {
const ServiceClass = this.statics[name]
this.instances[name] = new ServiceClass()
}
return this.instances[name]
}
} }
/** /**
* Register a class instance as a service. When requested, the provided * Fetch a container item by name.
* instance will be returned as the instance of the service. The instance's * @deprecated Please use Container.get from now on. This will be removed in the future.
* constructor is saved as the service definition. * @param {string} name
* @param {string} service_name - the referential name of the service * @returns {module:flitter-di/src/Service~Service|*}
* @param {module:flitter-di/src/Service~Service} service_instance - the service class instance
*/ */
register_as_instance(service_name, service_instance) { service(name) {
this.definitions[service_name] = service_instance.constructor return this.get(name)
this.instances[service_name] = service_instance
this._process_deferral(service_name, service_instance)
} }
/** /**
* Process deferred classes that need the provided service name and instance. * Process deferred classes that need the provided service name and instance.
* @param {string} service_name - the referential name of the service * @param {string} item_name - the referential name of the IoC item
* @param {module:flitter-di/src/Service~Service} service_instance - the instance of the service * @param {module:flitter-di/src/Service~Service|*} item - the Service or item to be injected
* @private * @private
*/ */
_process_deferral(service_name, service_instance) { _process_deferral(item_name, item) {
const new_deferrals = [] const new_deferrals = []
for ( const Class of this.deferred_classes ) { for ( const Class of this.deferred_classes ) {
if ( Class._di_deferred_services.includes(service_name) ) { if ( Class._di_deferred_services.includes(item_name) ) {
Class.__deferral_callback(service_name, service_instance) Class.__deferral_callback(item_name, item)
} }
if ( Class.__has_deferred_services ) { if ( Class.__has_deferred_services ) {
@ -143,14 +190,14 @@ class Container {
} }
/** /**
* Defer a static class to have its missing services filled in as they * Defer a static class to have its missing IoC items filled in as they
* become available in the service container. The class should extend * become available in the service container. The class should extend
* from Injectable. * from Injectable.
* @param {*} Class - the static class to be deferred * @param {*} Class - the static class to be deferred
*/ */
defer(Class) { defer(Class) {
if ( !this.__is_deferrable(Class) ) { if ( !this.__is_deferrable(Class) ) {
throw new Error('Cannot defer non-deferrable class: '+Class.name) throw new TypeError('Cannot defer non-deferrable class: '+Class.name)
} }
this.deferred_classes.push(Class) this.deferred_classes.push(Class)

@ -2,6 +2,9 @@
* @module flitter-di/src/DependencyInjector * @module flitter-di/src/DependencyInjector
*/ */
/**
* @type {typeof module:flitter-di/src/Container~Container}
*/
const Container = require('./Container') const Container = require('./Container')
/** Manages services and injects classes from its service container. */ /** Manages services and injects classes from its service container. */
@ -16,14 +19,27 @@ class DependencyInjector {
} }
/** /**
* Inject a static class with the services it requests. This mutates * Instantiate the passed in class. If it is injectable, it will be injected.
* the class' prototype. * @param {typeof module:flitter-di/src/Injectable~Injectable} Class
* @param Class * @param {...*} args - additional arguments to be passed to the constructor of the class
* @returns {*} - the injected static reference to the Class * @returns {*} - the injected static reference to the Class
*/ */
make(Class) { make(Class, ...args) {
if ( this.__is_injectable(Class) ) {
this.inject(Class)
}
return new Class(...args)
}
/**
* Inject the passed in Class with the IoC items it requires.
* @param {typeof module:flitter-di/src/Injectable~Injectable} Class
* @returns {*}
*/
inject(Class) {
if ( !this.__is_injectable(Class) ) { if ( !this.__is_injectable(Class) ) {
throw new Error('Cannot inject non-injectable class: '+Class.name) throw new TypeError(`Cannot inject non-injectable class: ${Class.name}`)
} }
Class.__inject(this.container) Class.__inject(this.container)
@ -34,11 +50,21 @@ class DependencyInjector {
* Fetch a service by name. If no name is provided, return the service * Fetch a service by name. If no name is provided, return the service
* proxy container. This container has getters for all the services by * proxy container. This container has getters for all the services by
* name. * name.
* @param {string} service - the name of the service * @deprecated - prefer DependencyInjector.get. This will be removed in the future.
* @param {string} name - the name of the service
* @returns {module:flitter-di/src/Service~Service|undefined|Proxy} - the service instance or service container proxy * @returns {module:flitter-di/src/Service~Service|undefined|Proxy} - the service instance or service container proxy
*/ */
service(name) { service(name) {
return this.container.service(name) return this.container.get(name)
}
/**
* Fetch an IoC item by name.
* @param {string} name
* @returns {*}
*/
get(name) {
return this.container.get(name)
} }
/** /**
@ -68,7 +94,7 @@ class DependencyInjector {
Module.prototype.require = function() { Module.prototype.require = function() {
const value = original_require.apply(this, arguments) const value = original_require.apply(this, arguments)
return di.__is_injectable(value) ? di.make(value) : value return di.__is_injectable(value) ? di.inject(value) : value
} }
} }

@ -58,11 +58,11 @@ class Injectable {
this.services.forEach(name => { this.services.forEach(name => {
if ( !this._di_allow_defer ) { if ( !this._di_allow_defer ) {
// If this class' services aren't deferrable, then just fetch their instances // If this class' services aren't deferrable, then just fetch their instances
this.prototype[name] = container.service(name) this.prototype[name] = container.get(name)
} else { } else {
// Otherwise, grab them if they exist, or put in deferral requests // Otherwise, grab them if they exist, or put in deferral requests
if ( container.has(name) ) { if ( container.has(name) ) {
this.prototype[name] = container.service(name) this.prototype[name] = container.get(name)
} else { } else {
this._di_deferred_services.push(name) this._di_deferred_services.push(name)
} }

@ -0,0 +1,7 @@
class MissingContainerDefinitionError extends TypeError {
constructor(item_name) {
super(`An item with the name "${item_name}" could not be found in the DI Container.`)
}
}
module.exports = exports = MissingContainerDefinitionError

@ -0,0 +1,29 @@
const { Container, DependencyInjector, Service } = require("./index")
class ServiceA extends Service {
num = 1.34
}
class ServiceB extends Service {
static get services() {
return ['service_a', 'num_pi']
}
get num() {
return this.service_a.num * this.num_pi
}
}
const ioc_container = new Container({
service_a: ServiceA,
})
ioc_container.register_singleton('num_pi', 3.141)
ioc_container.register_service('service_b', ServiceB)
const ioc_di = new DependencyInjector(ioc_container)
global.cont = ioc_container
global.di = ioc_di
global.AA = ServiceA
global.BB = ServiceB
Loading…
Cancel
Save