@6pm/depend 中文文档教程

发布于 7年前 浏览 29 项目主页 更新于 3年前

@6pm/depend

 npm 版本”></a> <a href=依赖状态 构建状态 Coverage Status

声明式依赖注入。

Table of contents

Install

npm install @6pm/depend --save

Test

运行单元测试套件。

git clone https://github.com/6pm-js/depend.git
cd depend
npm install
npm test

运行 nyc 覆盖率测试。

git clone https://github.com/6pm-js/depend.git
cd depend
npm install
npm run cover

这将导致文本摘要显示和详细的 html 覆盖 在 /coverage 中创建的报告。

Usage

使用需要两个不同的组件,依赖声明和 注入点 - 具体实现和创建的运行时绑定 实例/满足依赖性。

Declarative vs imperative

为什么支持两个非标准的 ES 扩展? 两个类属性,和 装饰器处于不断变化的状态,在即将到来的 ES 规范中还不稳定 (在撰写本文时),但有可用的实现,并且 关于声明式方法,有一些非常可读的东西。

命令式机制允许与现有的向后兼容集成 库,或者更安全路径,如果需要的话——但是声明式的方法是 个人喜好,因为我发现它不那么令人惊讶,而且更多 明确的意图。

不需要工具来允许声明性的装饰器方法, 除非你打算在你的项目中使用它,并且没有能力是独一无二的 无论哪种方法 - 选择您喜欢的任何一种。

为了使用声明式方法(在撰写本文时),两个 需要通过 Babel 转译进行转换:

Declaring dependencies

使用声明性装饰器语法

import { Inject } from '@6pm/depend';
import { InjectedType } from 'wherever'

class SomeClass {

    @Inject(InjectedType)
    property = null;

}

或命令式语法声明依赖项:

import { Depend } from '@6pm/depend';
import { InjectedType } from 'wherever'

class SomeClass {}

Depend.inject(SomeClass, 'property', InjectedType);

依赖项可以附加到整个类,在这种情况下,它们适用于 构造函数,在通过 a 创建时向其提供参数 Container

import { Inject, Depend } from '@6pm/depend';
import { InjectedTypeA, InjectedTypeB } from 'wherever'

// Declarative approach
@Inject(InjectedType, InjectedTypeB)
class SomeClass1 {

    constructor(injectedInstanceA, injectedInstanceB) {}

}


// Imperative approach
class SomeClass2 {

    constructor(injectedInstanceA, injectedInstanceB) {}

}
Depend.inject(SomeClass, null, InjectedTypeA, InjectedTypeB);
// The ^null is important, it specifies that we're not targeting a property of
// SomeClass2, but the constructor.

遗留装饰器插件,用于 Babel,不允许装饰 直接构造函数——这就是为什么声明式方法必须装饰 类,而不是实际的 constructor 函数。

依赖项也可以直接注入到类方法中,这些方法被处理 作为 setter 函数:

import { Inject, Depend } from '@6pm/depend';
import { DatabaseAbstraction } from './database.js'

// Declarative approach
class SomeClass1 {

    @Inject(DatabaseAbstraction)
    setDatabaseAbstraction(dbAPI) {}
}


// Imperative approach
class SomeClass2 {

    setDatabaseAbstraction(dbAPI) {}

}
Depend.inject(SomeClass, 'setDatabaseAbstraction', DatabaseAbstraction);

Creating instances, and satisfying dependencies

一旦一个类被声明为由 depend 管理,它不应该被手动 使用 new 实例化,而是通过托管 Container 创建。 这 Container 确保声明的任何依赖项都得到满足,包括 返回实例之前的子依赖项。

import { Inject, Container } from '@6pm/depend';
import { DatabaseAbstraction } from './database.js'

@Inject(DatabaseAbstraction)
class SomeClass {

    constructor(databaseAbstraction) {
        // This will be called prior to create, below, completing.
    }

}

// A new instance of DatabaseAbstraction will be created, and provided to the
// SomeClass constructor, prior to the new SomeClass instance being returned.
let instance = new Container().create(SomeClass);

Runtime determination of concrete implementations

Container 类不仅仅创建实例,而且满足 dependencies - 它也可以用于指定要使用的替代实现 在运行时,当遇到特定的依赖类型时。

这是使用 Container 实例上的 .bind() 方法实现的。

import { Inject, Container } from '@6pm/depend';
import { DatabaseAbstraction } from './database.js'
import { DatabaseMySQL } from './database-mysql.js'

@Inject(DatabaseAbstraction)
class SomeClass {

    constructor(databaseAbstraction) {
        // This will be called prior to create, below, completing.
    }

}

// A new instance of DatabaseMySQL will be created, and provided to the
// SomeClass constructor, prior to the new SomeClass instance being returned.
let instance = new Container()
    .bind(DatabaseAbstraction, DatabaseMySQL)
    .create(SomeClass);

Post injection intialisation

每个类的单个函数可以标记为 init 函数,这将是 在新实例上完成所有依赖注入后执行 拥有的类,像这样:

import { Init } from '@6pm/depend';

class SomeClass {

    @Init
    someInitFunction() {
        // This will only execute once a Container.created instance of SomeClass
        // has been fully created, and all dependencies injected.
    }

}

或者,命令式地:

import { Depend } from '@6pm/depend';

class SomeClass {

    someInitFunction() {
        // This will only execute once a Container.created instance of SomeClass
        // has been fully created, and all dependencies injected.
    }

}

Depend.init(SomeClass, 'someInitFunction');

Inheritance

依赖使用 ES2016 类格式继承,或者传统的 原型继承。 所以扩展托管类会自动赋予 所有要求,并确定超载要求。

class A {

    @Inject(SomeType)
    property;

}

class B extends A {}

class C extends A {

    @Inject(AlternativeType)
    property;

}

在上面的示例中,创建 B 的实例会将 SomeType 注入到 property,作为继承的要求。 创建实例 C 将注入 AlternativeType 的实例,而不需要或创建 SomeType 的实例首先。

Typical phases of a dependency injected application

依赖注入应用程序的阶段通常是:

  • 意图声明/依赖项

    • Using @Inject, @Abstract and @Singleton to declare intent, and requirements.
    • This typically occurs throughout the application, where ever components reside.
  • 配置/绑定

    • Creating a Container instance, and binding concrete implementations
    • This typically occurs in a single setup file that applies configuration for a given environment.
  • 运行时使用

    • Wherever new instances are required, the Container instance created in the configuration / binding step is used to create new instances, and automatically satisfy any required dependencies.

例如:

  • Declaration:

./service.js;

import { Inject, Singleton } from '@6pm/depend';

class Service {

    @Inject(Logger)
    logger;

    start() {
        logger.log('Started!');
    }
}

<代码>./interface/someLogger.js;

class Logger {

    @Abstract
    log(message) {}

}

<代码>./implementation/console-logger.js;

class ConcreteConsoleLogger {

    log(message) {
        console.log(message);
    }

}
  • Configuration / binding

./configuration/container.js

import { Container } from '@6pm/depend';

import Logger from './interface/logger';
import ConcreteLogger from './implementationconsole-logger';

function configure() {
    let container = new Container();

    container.bind(Logger, ConcreteLogger);

    return container;
}

export let container = configure();
  • Runtime usage:

main.js

import Service from './service.js';
import container from './configuration/container.js';

let service = container.create(Service);
service.start();

As Promised

某些依赖项在访问远程之前无法准备好注入 资源,是否获取配置,或连接,资源等

。为了促进这一点,而不引入令人惊讶的方差水平 在返回类型中,Promise 可能会从三个关键点返回 依赖注入生命周期:

  • constructor
  • injected property setter
  • init

如果上述任何方法返回一个 Promise 那么 container.create 也会 返回一个解析为新实例的 Promise,当且仅当有且 所有返回的 Promise 成功解析 - 或者拒绝第一个 拒绝。

如果上述方法的所有三个(或任意组合)都返回 Promise,那么 他们保证在上面列出的顺序中解决,并在相同的顺序中拒绝 命令。

如果在单个类上声明了多个注入的 setter,并返回多个 Promise,那么它们将无序地有效地同时发生 它们之间的保证,但它们将使用 Promise.all 之前进行同步 到正在执行的 init 函数。

Downstream dependencies as Promises

为了:

  1. Prevent surprise (and cascading requirements to handle Promises)
  2. Ensure control flow options remain the preserve of the application developer

返回 Promise 的下游依赖项 DO NOT 自动提升 Promise 的上游类型。

也就是说,如果类 A 声明了一个实例的依赖注入 B 类,而 B 是一个 Promise,创建 A 的实例不会产生结果 在 Promise 中,除非它显式返回 Promise,如 上一节。

上面说了,这样可以防止意外(具体实现C是 绑定为运行时配置,并解析为 Promise,其中 B 没有), 并确保应用程序员可以选择如何处理这样的 情况 - A 的依赖注入实例可能不需要下游 在创建之前从 Promise 中解析依赖项 - 自动这样做会在步骤之间创建自动同步性, 例如,防止处理 Promise 的解决能力,并触发 结果,异步地处理任何进一步的逻辑。

因此,处理此类下游依赖项留给 应用程序开发人员,以及解析为 Promise 的任何注入依赖项 只会传递 Promise - 如果依赖注入应该等待 Promised 依赖的解析,可以手动级联,通过返回 来自前面提到的任何生命周期方法的 Promise,或者使用 async / await 语法,如果可用的话,像这样:

import { Inject, Container } from '@6pm/depend';
import { DatabaseAbstraction } from './database.js'

@Inject(DatabaseAbstraction)
class SomeService {

    async constructor(databaseAbstraction) {
        this.db = await databaseAbstraction;
        return this;
    }

    @Init
    init() {
        // This method will only be called if the construction succeeded, and
        // the `.db` property has successfully resolved, and is available.
    }

}


async function startService() {
    try{
        let someService = await new Container()
            // .bind() any runtime configuration here
            .create(SomeService);

        // someService will now exist, with a fully resolved `.db` property.
        someService.start();

    }catch(e) {
        // someService failed to instantiate, the causal error (from
        // DatabaseAbstraction, or the bound concrete implementation, thereof),
        // will be caught here.
    }
}

startService();

// Alternatively, using the raw `Promise` flow.
new Container()
    // .bind() any runtime configuration here
    .create(SomeService)
    .then(function(someService) {
        // Fully resolved `someService` instance now available to use, with a
        // resolved `.db` property.
        someService.start();
    }).catch(function(error) {
        // If the `DatabaseAbstraction` rejects for some reason, `someService`
        // instantiation will fail, calling this function with the causal Error.
    });

Singletons

除了提供类型的实例,并在 运行时,某些类可能被设计为共享,只需要一个 一个实例,应该传递给任何已声明的依赖项。 这个可以 通过使用声明式将一个类声明为单例来实现 装饰器,或命令式风格。

import { Singleton, Depend } from '@6pm/depend';

@Singleton
class A {}

class B {}
Depend.singleton(B);

任何声明依赖于 AB 的类都只会实例化一个 实例,并且所有人都将收到该单个实例。

这对于注入常见的实现特别有用,例如 数据存储抽象(也就是说,在后台处理连接池),或者 一个配置的日志记录接口,或有争议的仲裁机制 资源,常见的应用程序配置等

Interfaces in a language without interfaces

。JavaScript 没有任何真正的接口概念,所以依赖库 提供了一种机制来允许对概念进行有效的近似。

方法或整个类可以通过装饰器或 命令式方法,如下所示:

import { Abstract, Depend } from '@6pm/depend';

@Abstract
class A {}

class B {

    @Abstract
    someMethod() {}

}

class C {}
Depend.abstract(C);

抽象属性自动授予所属类,如果一个方法 被声明为抽象的,并具有以下效果:

  • For methods - 任何调用该方法的尝试都将抛出一个 Error 方法是抽象的,应该在子类中重载。

  • 对于类 - 任何通过容器创建类实例的尝试, 将抛出一个 Error 表明这个类是抽象的,而一个具体的 应该使用类的扩展。

虽然依赖库和 JavaScript 不验证合约 接口被接受,这增加了一定程度的安全性,因为 bind a 失败 具体实施将导致创建新实例失败, 是否应该在任何阶段将抽象类作为依赖项请求 - 这 确保在启动时快速失败 - 并有可能发现错误 在生产配置的集成测试期间配置。

Destructured injection

除了指定在依赖注入期间要解析的类型之外, 依赖库可以接受有限形式的解构赋值,提供 更复杂的结构,例如数组字面量和对象字面量,如下所示:

@Inject({ database: DatabaseAbstraction, logger: LoggerAbstraction })
class SomeClass {

    constructor(options) {
        this.database = options.database;
        this.logger = options.logger;
    }

}

将遍历和注入任何深度的数组和对象字面量定义 根据需要 - 这与 ES2016 解构分配特别好:

@Inject([ { sub: A }, [ B, C ] ])
class SomeClass {

    constructor([ { sub:a }, [ b, c ] ]) {
        // a, b and c receive instances of A, B and C respectively.
    }

}

Cyclic dependencies

依赖库自动确定依赖项是否导致循环,两者 在声明依赖关系时以及绑定具体时 实现,如果发生这种情况将抛出 - 因为周期性 永远无法正确满足依赖关系。

如果 A 需要 B 的实例,则需要 C 的实例, 然后需要 A,无法确定 satisfied 状态 循环链上的任何实例。

如果检测到循环,则会从依赖项中抛出一个 Error 声明(@InjectDepend.inject),或导致的 bind` 调用 周期。

Performance

依赖库被设计成非常高效的,使用两种主要方法

  • : 新实例尽可能便宜。 特别是,循环检测发生 在依赖声明和容器绑定阶段 - 确保成本不会影响运行时性能和使用模式。

  • 动态代码生成,允许 ES 运行时/编译器进行优化 尽可能多的工作——虽然这是一项有争议的技术,但这是永远看不到的 在外部,但在开发过程中进行测试(通过微基准测试 - YMMV)证明 在实例创建方面提供超过 3 个数量级的差异。

此外,动态代码生成,特别是在 v8 上,增加了非常 启动开销很小。

Contributing

欢迎所有问题、请求、问题、PR、改进或仅仅是评论! 我所要求的只是尝试符合相当宽松的房屋风格,如果打开 拉请求!

@6pm/depend

npm version dependencies Status Build Status Coverage Status

Declarative dependency injection.

Table of contents

Install

npm install @6pm/depend --save

Test

To run the unit test suite.

git clone https://github.com/6pm-js/depend.git
cd depend
npm install
npm test

To run nyc coverage tests.

git clone https://github.com/6pm-js/depend.git
cd depend
npm install
npm run cover

This will result in a textual summary display, and a detailed html coverage report created in /coverage.

Usage

Usage requires two distinct components, declaration of dependencies and injection points - and runtime binding of concrete implementations, and creation of instances / satisfying of dependencies.

Declarative vs imperative

Why support two non-standard ES extensions? Both class properties, and decorators are in a state of flux, not yet stable in any upcoming ES spec (at the time of writing), but there are available implementations that work, and there is something eminently readable about declarative approaches.

The imperative mechanism allows backwards compatible integration with existing libraries, or the safer path, if desired - but the declarative approach is a personal preference, as I find it considerably less surprising, and more explicit about intent.

There is no need to intrument to allow the declarative, decorator approach, unless you intend to use it within your project, and no capability is unique to either approach - choose whichever you are comfortable with.

In order to use the declarative approach (at the time of writing), two transforms are needed, via Babel transpilation:

Declaring dependencies

Dependencies are declared using either the declarative decorator syntax:

import { Inject } from '@6pm/depend';
import { InjectedType } from 'wherever'

class SomeClass {

    @Inject(InjectedType)
    property = null;

}

or imperative syntax:

import { Depend } from '@6pm/depend';
import { InjectedType } from 'wherever'

class SomeClass {}

Depend.inject(SomeClass, 'property', InjectedType);

Dependencies can be attached to the whole class, in which case they apply to the constructor function, supplying the parameters to it, when created via a Container:

import { Inject, Depend } from '@6pm/depend';
import { InjectedTypeA, InjectedTypeB } from 'wherever'

// Declarative approach
@Inject(InjectedType, InjectedTypeB)
class SomeClass1 {

    constructor(injectedInstanceA, injectedInstanceB) {}

}


// Imperative approach
class SomeClass2 {

    constructor(injectedInstanceA, injectedInstanceB) {}

}
Depend.inject(SomeClass, null, InjectedTypeA, InjectedTypeB);
// The ^null is important, it specifies that we're not targeting a property of
// SomeClass2, but the constructor.

The legacy decorator plugin, for Babel, does not allow decoration of constructors directly - which is why the declarative approach must decorate the class, rather than the actual constructor function.

Dependencies can also be injected directly into class methods, which are treated as setter functions:

import { Inject, Depend } from '@6pm/depend';
import { DatabaseAbstraction } from './database.js'

// Declarative approach
class SomeClass1 {

    @Inject(DatabaseAbstraction)
    setDatabaseAbstraction(dbAPI) {}
}


// Imperative approach
class SomeClass2 {

    setDatabaseAbstraction(dbAPI) {}

}
Depend.inject(SomeClass, 'setDatabaseAbstraction', DatabaseAbstraction);

Creating instances, and satisfying dependencies

Once a class is declared as managed by depend, it should not be manually instantiated with new, but instead created via a managed Container. The Container ensures that any dependencies declared are satisified, including sub-dependencies before returning an instance.

import { Inject, Container } from '@6pm/depend';
import { DatabaseAbstraction } from './database.js'

@Inject(DatabaseAbstraction)
class SomeClass {

    constructor(databaseAbstraction) {
        // This will be called prior to create, below, completing.
    }

}

// A new instance of DatabaseAbstraction will be created, and provided to the
// SomeClass constructor, prior to the new SomeClass instance being returned.
let instance = new Container().create(SomeClass);

Runtime determination of concrete implementations

The Container class does much more than just create instances, and satisfy dependencies - it can also be used to specify alternate implementations to use at runtime, when a particular dependency type is encountered.

This is achieved using the .bind() method on the Container instance.

import { Inject, Container } from '@6pm/depend';
import { DatabaseAbstraction } from './database.js'
import { DatabaseMySQL } from './database-mysql.js'

@Inject(DatabaseAbstraction)
class SomeClass {

    constructor(databaseAbstraction) {
        // This will be called prior to create, below, completing.
    }

}

// A new instance of DatabaseMySQL will be created, and provided to the
// SomeClass constructor, prior to the new SomeClass instance being returned.
let instance = new Container()
    .bind(DatabaseAbstraction, DatabaseMySQL)
    .create(SomeClass);

Post injection intialisation

A single function per class may be marked as an init function, which will be executed after all dependency injection has been completed on a new instance of the owning class, like so:

import { Init } from '@6pm/depend';

class SomeClass {

    @Init
    someInitFunction() {
        // This will only execute once a Container.created instance of SomeClass
        // has been fully created, and all dependencies injected.
    }

}

or, imperatively:

import { Depend } from '@6pm/depend';

class SomeClass {

    someInitFunction() {
        // This will only execute once a Container.created instance of SomeClass
        // has been fully created, and all dependencies injected.
    }

}

Depend.init(SomeClass, 'someInitFunction');

Inheritance

Dependencies are inherited using the ES2016 class format, or traditional prototypical inheritance. So extending a managed class automatically confers all requirements, and determines overloaded requirements, too.

class A {

    @Inject(SomeType)
    property;

}

class B extends A {}

class C extends A {

    @Inject(AlternativeType)
    property;

}

In the above example, creating an instance of B will inject SomeType into property, as an inherited requirement. Creating an instance C will inject an instance of AlternativeType, instead, without requiring, or creating an instance of SomeType first.

Typical phases of a dependency injected application

The phases of a dependency injected application are typically:

  • Declaration of intent / dependencies

    • Using @Inject, @Abstract and @Singleton to declare intent, and requirements.
    • This typically occurs throughout the application, where ever components reside.
  • Configuration / binding

    • Creating a Container instance, and binding concrete implementations
    • This typically occurs in a single setup file that applies configuration for a given environment.
  • Runtime usage

    • Wherever new instances are required, the Container instance created in the configuration / binding step is used to create new instances, and automatically satisfy any required dependencies.

As an example:

  • Declaration:

./service.js;

import { Inject, Singleton } from '@6pm/depend';

class Service {

    @Inject(Logger)
    logger;

    start() {
        logger.log('Started!');
    }
}

./interface/someLogger.js;

class Logger {

    @Abstract
    log(message) {}

}

./implementation/console-logger.js;

class ConcreteConsoleLogger {

    log(message) {
        console.log(message);
    }

}
  • Configuration / binding

./configuration/container.js

import { Container } from '@6pm/depend';

import Logger from './interface/logger';
import ConcreteLogger from './implementationconsole-logger';

function configure() {
    let container = new Container();

    container.bind(Logger, ConcreteLogger);

    return container;
}

export let container = configure();
  • Runtime usage:

main.js

import Service from './service.js';
import container from './configuration/container.js';

let service = container.create(Service);
service.start();

As Promised

Some dependencies cannot be ready for injection until they have accessed remote resources, whether to obtain configuration, or connections, resources, etc.

In order to facilitate this, without introducing surprising levels of variance in return types, Promises may be returned from three key points within the dependency injection lifecycle:

  • constructor
  • injected property setter
  • init

If any of the above methods return a Promise then container.create will also return a Promise that resolves to the new instance if, and only if any and all Promises returned resolve successfully - or reject on the first that rejects.

If all three (or any combination) of the above methods return Promises, then they are guaranteed to resolve in the oder listed above, and reject in the same order.

If multiple injected setters are declared on a single class, and return multiple Promises, then they will occur, effectively, simultaneously, with no order guarantees between them, but they will be sychronised using Promise.all prior to an init function being executed.

Downstream dependencies as Promises

In order to:

  1. Prevent surprise (and cascading requirements to handle Promises)
  2. Ensure control flow options remain the preserve of the application developer

Downstream dependencies that return Promises DO NOT automatically promote an upstream type to a Promise.

That is, if, say, class A declares a dependency injection of an instance of class B, and B is a Promise, creating an instance of A will not result in a Promise, unless it explicitly returns a Promise as described in the previous section.

As mentioned above, this prevents surprise (concrete implementation C is bound as runtime configuration, and resolves to a Promise, where B did not), and ensures that the application programmer can choose how to handle such a situation - the dependency injected instance of A may not need the downstream dependency to be resolved from a Promise prior to being created - automatically doing so would create an automatic synchronicity between steps, preventing, say, the ability to handle resolving of a Promise, and triggering of any further logic as a consequence, asynchronously.

Handling of such downstream dependencies is therefore left as an exercise to the application developer, and any injected dependencies that resolve to a Promise will merely deliver the Promise - if dependency injection should wait for resolution of a Promised dependency, that can cascaded manually, by returning a Promise from any of the previously mentioned lifecycle methods, or, using async / await syntax, if available, like so:

import { Inject, Container } from '@6pm/depend';
import { DatabaseAbstraction } from './database.js'

@Inject(DatabaseAbstraction)
class SomeService {

    async constructor(databaseAbstraction) {
        this.db = await databaseAbstraction;
        return this;
    }

    @Init
    init() {
        // This method will only be called if the construction succeeded, and
        // the `.db` property has successfully resolved, and is available.
    }

}


async function startService() {
    try{
        let someService = await new Container()
            // .bind() any runtime configuration here
            .create(SomeService);

        // someService will now exist, with a fully resolved `.db` property.
        someService.start();

    }catch(e) {
        // someService failed to instantiate, the causal error (from
        // DatabaseAbstraction, or the bound concrete implementation, thereof),
        // will be caught here.
    }
}

startService();

// Alternatively, using the raw `Promise` flow.
new Container()
    // .bind() any runtime configuration here
    .create(SomeService)
    .then(function(someService) {
        // Fully resolved `someService` instance now available to use, with a
        // resolved `.db` property.
        someService.start();
    }).catch(function(error) {
        // If the `DatabaseAbstraction` rejects for some reason, `someService`
        // instantiation will fail, calling this function with the causal Error.
    });

Singletons

In addition to supplying instances of types, and binding them dynamically at runtime, certain classes may be designed to be shared, requiring one, and only one instance, which should to be passed to any declared dependency. This can be achieved by declaring a class as a singleton, using either a declarative decorator, or imperative style.

import { Singleton, Depend } from '@6pm/depend';

@Singleton
class A {}

class B {}
Depend.singleton(B);

Any class declaring a dependency of A, or B will only instantiate a single instance, and all will receive that single instance.

This is particularly useful for injecting common implementations, such as a datastore abstraction (that, say, handles connection pooling under the hood), or a configured logging interface, or arbitration mechanism for contentious resources, common application configuration, etc.

Interfaces in a language without interfaces

JavaScript does not have any real concept of interfaces, so the depend library provides a mechanism to allow an effective approximation of the concept.

Methods, or entire classes, can be declared as abstract via the decorator or imperative approach, as so:

import { Abstract, Depend } from '@6pm/depend';

@Abstract
class A {}

class B {

    @Abstract
    someMethod() {}

}

class C {}
Depend.abstract(C);

The abstract property is automatically conferred to the owning class, if a method is declared abstract, and has the following effects:

  • For methods - any attempt to invoke the method will throw an Error that the method is abstract, and should be overloaded in a subclass.

  • For classes - any attempt to create an instance of the class, via a container, will throw an Error indicating that the class is abstract, and a concrete extension of the class should be used instead.

While the depend library, and JavaScript do not validate that the contract of an interface is honoured, this adds a degree of safety, as a failure to bind a concrete implementation will result in the creation of a new instance failing, should an abstract class be requested at any stage as a dependency - which ensures fast failure, at startup - and the potential to catch an erroneous configuration during an integration test of production configuration.

Destructured injection

In addition to specifying types to be resolved during dependency injection, the depend library can accept a limited form of destructured assignment, providing more complex structures, such as array literals, and object literals, like so:

@Inject({ database: DatabaseAbstraction, logger: LoggerAbstraction })
class SomeClass {

    constructor(options) {
        this.database = options.database;
        this.logger = options.logger;
    }

}

Any depth of array and object literal definitions will be traversed and injected as needed - which works particularly well with ES2016 destructured assignment:

@Inject([ { sub: A }, [ B, C ] ])
class SomeClass {

    constructor([ { sub:a }, [ b, c ] ]) {
        // a, b and c receive instances of A, B and C respectively.
    }

}

Cyclic dependencies

The depend library automatically determines if dependencies cause cycles, both at the point of declaration of dependencies, and when binding concrete implementations, and will throw if such a situation occurs - since cyclical dependencies can never be correctly satisfied.

If A requires an instance of B, that then requires and instance of C, that then requires A, there is no way to determine a satisfied state for any instance along the cyclical chain.

If a cycle is detected, an Error is thrown from either the dependency declaration (the @Inject or Depend.inject), or thebind` call that causes the cycle.

Performance

The depend library is designed to be very performant, using two primary methods:

  • Front loading as much work as possible, to ensure that creating, and satisfying new instances is as cheap as possible. In particular, cycle detection occurs during the declaration of dependencies, and the container binding stage - ensuring that the cost does not affect run time performance, and usage patterns.

  • Dynamic code generation, to allow the ES runtime / compiler to optimise away as much work as possible - while a contentious technique, this is never visible externally, but testing during development, (via microbenchmarks - YMMV), proved to provide more than 3 orders of magnitude difference in instance creation.

In addition, the dynamic code generation, on v8 in particular, adds very little startup overhead.

Contributing

All issues, requests, questions, PRs, improvements, or merely comments welcome! All I ask is to attempt to conform to the rather loose house style, if opening a pull request!

更多

友情链接

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文