Services
The goal of services is to move away from global variables. This is achieved by requesting a reference to them instead of accessing them directly.
You can access a service by getting it from the service container in a few different ways.
container.get(serviceKey); // using the service key
container.get(MyService); // using the service class
container.get(IMyService); // using the interface
You can use the serviceKey ( symbol ) directly
or use a class that has a get serviceKey()
method.
The interface
in this case is just an abstract class
with a get serviceKey()
method.
It should have abstract declarations of
the public methods and props of the actual class.
This gives us code-completion and compile time validation.
To make requesting services smoother dependencies were introduced.
Declaring Dependencies
In zox.js you can request that values be assigned to your class properties by marking them as Dependencies.
TypeScript:
export class MyClass
{
@Dependency
private config: IConfigService;
constructor(props) { /* .. */ }
public work() { /* .. */ }
}
JavaScript:
export class MyClass
{
constructor(props) { /* .. */ }
work() { /* .. */ }
}
Dependency(IConfigService)(MyClass, 'config');
In this example we requested that the IConfigService
gets injected into instances of MyClass
as the config
property.
Note
While we could have referenced the ConfigService directly we instead referenced it's interface IConfigService. This is because in general we should aim to program to an interface and not care about it's implementation.
Resolving Dependencies
When we create an instance of this class we will have to resolve it before we can use it's methods.
const myObj = new MyClass(props);
container.resolve(myObj, true);
Or to shorten that:
const myObj = container.create(MyClass, props);
Since the dependencies are assigned after our constructor is called
that means we have to move any setup we might have into the onResolved
method.
TypeScript:
export class MyClass implements IOnResolved
{
// ...
public onResolved()
{
this.options = this.config.getConfig('my.config.file');
}
}
JavaScript:
export class MyClass
{
// ...
onResolved()
{
this.options = this.config.getConfig('my.config.file');
}
}
In this example we loaded our config file and saved it in the options property.
You can learn more about the config service here.
Creating Services
While any class can be used as a service there is a pattern that you should stick to.
const serviceKey = Symbol('My Service');
export abstract class IMyService implements IService
{
get serviceKey(): symbol
{
return serviceKey;
}
public abstract work();
}
@Service
export class MyService extends IMyService implements IOnResolved
{
@Dependency // implicit service key from TypeScript metadata
private config: IConfigService;
@Dependency(IConfigService) // explicit service key from interface
private config1: IConfigService;
@Dependency(configServiceKey) // explicit service key
private config2: IConfigService;
public onResolved()
{
this.options = this.config.getConfig('my.config.file');
}
public work() { /* .. */ }
}
Since JavaScript has no concept of interfaces we defined our interface
as a class and gave it a unique serviceKey
.
We then extended this interface and implemented it's methods.
Note
You don't have to implement IOnResolved or have any dependencies, but since you probably will I've included it into this template.
If you don't like extending interfaces you could also implement
the get serviceKey()
method in the actual class.
The @Service
decorator in this example marks the class
as a service that should be registered on app start.
You can remove if you only intend to manually register this service.
Registering Services
Services can be registered as resolved or as unresolved, depending on if you have resolved them before registering them.
container.register(container.create(MyService));
container.registerUnresolved(new MyService());
The unresolved services will be resolved the first time the are requested.
Resolving services is not necessary if you know they have no dependencies.
container.register(new MyServiceWithoutDependencies());
In case you want to use a class without the get serviceKey()
method
as a service you will also have to assign a serviceKey to it.
container.registerAs(serviceKey, new AnyClass());
container.registerUnresolvedAs(serviceKey, new AnyClass());
You can also create a custom abstract class to be used as an interface for it.
container.registerAs(IAnyClass, new AnyClass());
The ServicePluginManager
can be used to automatically register
all services marked with the @Service
decorator.
Simple objects and functions can also be registered as services.
container.registerAs(serviceKey, { /* ... */ });
container.registerAs(serviceKey, function myFunc() { /* ... */ });
But due to the nature of services it is unlikely you will need something other than classes in most cases.