In the traditional modular design, we often declare various artifacts as
classes/interfaces in separate files and use export/import
to reference from
each other across modules.
For a large-scale application or framework, many kinds of artifacts and many instances of the same kind are added to the project. In many cases, there are also dependencies among them. It is desirable to have a consistent way to manage such artifacts so that they can be registered, configured, accessed, and resolved while respecting the dependency requirements.
Inversion of Control (IoC) is a proven design pattern to solve similar problems. Together with Dependency Injection (DI), artifacts become much more tangible and visible.
In LoopBack 4, we implemented such capabilities in the @loopback/context
module. The hierarchy of contexts becomes the universal knowledge base for the
whole application to promote visibility, extensibility, and composability.
Note:
The @loopback/core
package re-exports all public
APIs of @loopback/context
. For consistency, we recommend the usage of
@loopback/core
for imports in LoopBack modules and applications unless they
depend on @loopback/context
explicitly. The two statements below are
equivalent:
import {inject} from '@loopback/context';
import {inject} from '@loopback/core';
Let’s walk through some code snippets to illustrate how artifacts are managed
with @loopback/context
.
Registering artifacts
To register artifacts, we first create an instance of Context
and use bind
to add artifacts to the registry as bindings.
import {Context} from '@loopback/core';
import {GreetingController} from './controllers';
import {CACHING_SERVICE, GREETING_SERVICE} from './keys';
import {CachingService} from './caching-service';
import {GreetingService} from './greeting-service';
const ctx = new Context();
ctx.bind('controllers.GreetingController').toClass(GreetingController);
ctx.bind(CACHING_SERVICE).toClass(CachingService);
ctx.bind(GREETING_SERVICE).toClass(GreetingService);
export class GreetingApplication extends BootMixin(RestApplication) {
constructor(config: ApplicationConfig = {}) {
super(config);
this.projectRoot = __dirname;
this.add(createBindingFromClass(CachingService, {key: CACHING_SERVICE}));
this.add(createBindingFromClass(CachingInterceptor));
this.component(GreetingComponent);
}
// ...
}
Providing values for artifacts
Here are different ways to supply the value for the artifacts in LoopBack. Artifacts can be services, repositories, configuration, etc.
-
As a constant value.
The constant value can be a string, number or any other types.
-
To be created by a factory function.
For example, a function to get the current time.
-
To be instantiated from a class.
A class can be a service, controller or other LoopBack artifacts.
-
To be created from a provider class.
Since plain functions do not accept injections, you can create a wrapper class that has the ability to accept injection. For example, you can create a provider class that receives injection of a HTTP request.
-
As an alias to another binding
This is a convenient way to allow a binding pointing to another one.
The
binding-types.ts
in example-context
example shows various ways to provide values for a binding.
For details, see the Binding documentation page.
Resolving artifacts by key
The
find-bindings.ts
in the example-context
example shows different flavors of finding bindings in
a context
Discovering artifacts by filter
For the cases that we don’t know the binding keys ahead of time, we can match the bindings by using the filter function.
For example, controllers can be used for REST APIs, GraphQL APIs, web sockets, etc. We can discover all the controllers that has the OpenAPI decorators by having a filter function to differentiate different types of controllers.
Binding scopes
There are three binding scopes:
- Transient - a new instance of binding is created for each request.
- Singleton - there is only a single instance of the binding.
- Context - for a given context in the hierarchy, there is only a single instance for a given binding key.
For details, see Configure the scope.
Watching artifacts
The context view gives us control when a binding is being added or removed. This also allows the support of dynamic extension points. For the greeting-app example, after the application has started, more greeters can be added, and there is no need to add all the greeters up front.
See the Context documentation page and example for more details.
Contributing multiple artifacts via components
Components can be considered as a collection of binding added to the context. For example, for an authentication component, an authentication strategy and an authentication action can be added to the component.
See the GreetingComponent as an example.
/**
* Define a component to register the greeter extension point and built-in
* extensions
*/
export class GreetingComponent implements Component {
bindings = [
createBindingFromClass(GreetingService, {
key: GREETING_SERVICE,
}),
createBindingFromClass(EnglishGreeter),
createBindingFromClass(ChineseGreeter),
];
}
Previous: Part 2 - Architectural challenges