Vinyl uses the most common form of dependency injection; requesting dependencies through constructors.
Class constructors have a deps parameter describing the required dependencies.
E.g.
/**
* Dependencies for {@link RequesterImpl}
*/
export type RequesterImplDeps = {
readonly networkState: NetworkState
readonly fetch: Fetch
readonly networkMetricsController: NetworkMetricsController
}
export class RequesterImpl {
constructor(protected readonly deps: RequesterImplDeps) {}
// ...
}
const requester = new Requester({
networkState: new NetworkStateImpl(),
fetch: new FetchImpl(),
})
There are two steps to dependency injection, creating a record of dependency factories, then creating a container that uses the factories to construct implementations.
When creating a record of dependency factories, the keys match the dependency
properties, and the values are functions that produce a dependency. The factory
functions may accept a single argument indicating any dependencies required for
the implementation. Wrap the record in a call to validateFactories which is a
compile-time-only check that ensures the dependency graph is sound. That is,
there are no cyclic dependencies, missing dependencies, or incompatible types.
Example:
import { validateFactories } from '@amzn/vinyl-util'
class A {}
type BDependencies = {
readonly a: A
}
class B {
constructor(deps: BDependencies) {}
}
type CDependencies = {
readonly a: A
readonly b: B
}
class C {
constructor(deps: CDependencies) {}
}
const factories = validateFactories({
a: () => new A(),
b: (deps: BDependencies) => new B(deps),
c: (deps: CDependencies) => new C(deps),
} as const)
These factories will be used to create implementations. Use createContainer to
create a container that may be disposed and provides an object with getters to
lazily-construct implementations from the provided factories.
import { createContainer, createDisposer } from '@amzn/vinyl-util'
const { add, dispose } = createDisposer()
const deps = add(createContainer(factories)).dependencies
deps.a // A {}
deps.b // B {}
deps.c // C {}
Sometimes you need to inject existing instances into a dependency container
without transferring ownership. Use externalDependencies to wrap external
dependencies that should not be disposed when the container is disposed.
import {
externalDependencies,
validateFactories,
createContainer,
} from '@amzn/vinyl-util'
// External services managed elsewhere
const logger = new Logger()
const options = { port: 8080, host: 'localhost' }
const factories = validateFactories({
// These won't be disposed by the container
...externalDependencies({ logger, options }),
// This will be disposed by the container [if Server implements Disposable]
server: (deps: {
readonly options: ServerOptions
readonly logger: Logger
}) => new Server(deps.options, deps.logger),
})
const container = createContainer(factories)
// Use dependencies normally
container.dependencies.server.start()
// Only server.dispose() is called, logger and options are left alone
container.dispose()
As a general guideline, when defining a dependency to be provided, the interface requested should be the minimum API needed, and disposal should be the responsibility of the creator.