Diogenes
When asked why he went about with a lamp in broad daylight, Diogenes confessed, "I am looking for a [honest] man."
Diogenes helps to use the dependency injection pattern to split your application into reusable and testable components.
Dependency injection
The dependency injection pattern is a widely used design pattern. Simply put, allows to build complicated abstractions composed by simpler abstractions. The composition happens when you "inject" one or more dependency into a function:
const database = ;const passwordHashing = ;const users = ;
I call this progressively complex objects "services" as they provide a specific functionality.
While this is a very nice way to build an application, it will leave the developer with a lot of annoying problems:
- dealing with the boilerplate needed to create your services only when you need to (no need to rebuild a new "users" service in every module you need to use it)
- some service might return asynchronously
- you have to figure out and maintain the correct order for resolving the dependencies
- you have to manage errors step by step
Diogenes lets you design how these components interact between them, in an declarative way.
You declare your "services" and their dependencies:
const Diogenes = ;const registry = Diogenes; registry;registry; registry ;
and then get the service you need:
registry
Diogenes figures out the execution order, manages error propagation, deals with synchronous and asynchronous functions transparently and much more.
What is a service
A service is a unit of code with a name. It can be a simple value, a synchronous function (returning a value) or an asynchronous function returning a promise. It takes as argument an object containing the dependencies (output of other services).
A service outputs a "dependency", this is identified with the service name. Services are organised inside a registry. The common interface allows to automate how the dependencies are resolved within the registry.
A step by step example
Importing diogenes
You can import Diogenes using commonjs:
const Diogenes = ;
Creating a registry
You can create a registry with:
const registry = Diogenes; // or new Diogenes()
Defining services
A service is defined by a name (a string) and it can be as simple as a value:
registry ;
most of the time you will define a service as a function:
registry ;
If the function is asynchronous you can return a promise. It will work transparently:
const util = ;const fs = ;const readFile = util; registry ;
As you can see, Diogenes allows to mix sync and async functions.
Let's add other services:
registry ;
The method "dependsOn" allows to specify a list of dependencies. For example this service depends on the "text" service. The deps argument is an object containing an attribute for every dependency, in this example: deps.text.
registry ; registry ; registry ;
This is how services relates each other:
Calling a service
You can call a service using the method "run" on a registry.
registry
p will be the output of the paragraph service.
When resolving a dependency graph, diogenes takes care of executing every service at most once. If a service returns or throws an exception, this is propagated along the execution graph. Services getting an exception as one of the dependencies, are not executed.
Docstring
A docstring is the description of the service. That may help using diogenes-lantern, a tool that shows your registry with a graph. You can set a docstring like this:
registry
And you can retrieve a docString with:
registry
registry-runner
Registry runner is an object that takes care of running services. This adds many features to a simple registry. You can create a runner like this:
const registryRunner = Diogenes
then you can run a service with:
registryRunner
Registry runner allows to use callbacks:
registryRunner
The callback uses the node.js convention, the error is the first argument.
Another feature allows to execute multiple services efficiently using an array or a regular expression:
registryRunner
or the equivalent:
registryRunner
The result will be an object with an attribute for every dependency.
Using this feature is different to:
Promiseallregistry registry registry
Because it ensures that every service is executed at most once.
You can also use the same method to add services without dependencies, without changing the original registry:
registryRunner
So if a service depends on "times", it will get 3. This can be useful for testing (injecting a mock in the dependency graph). It is also useful to give an "execution context" that is different every time (think for example the request data in a web application).
The registry runner keeps track of all pending execution so is able to gracefully shutdown:
registryRunner registryRunner // this return a promise rejection because the registry is not accepting new tasks
Registry and decorators
The decorator pattern can be very useful to enhance a service. For example adding a caching layer, logging or to convert a callback based service to use a promise (promisify is a decorator). The method "provides" includes a shortcut to add decorators to the service. If you pass an array or more than one argument, to the method. In the next example I am able to add a service that uses a callback instead of promises:
registry
In the next example I use a decorator to ensure a service is executed only once:
// define my decoratorconst onlyOnce = { let cache return { if typeof cache === 'undefined' cache = return cache } } registry
You can add multiple decorators:
registry
This is the equivalent of:
registry
You can find many examples of what you can do with decorators on async-deco and on diogenes-utils. This one in particular, contains a decorator that caches a service.
const cacheService = cacheServiceregistry
Syntax
Diogenes.getRegistry
Create a registry of services:
const registry = Diogenes;
or
const registry = ;
Diogenes.getRegistryRunner
Create a registry runner instance:
const registry = Diogenes;
Registry
service
Returns a single service. It creates the service if it doesn't exist.
registry;
init
Helper function. It runs a group of functions with the registry as first argument. Useful for initializing the registry.
/* module1 for example */module { registry;};/* main */const module1 = ;const module2 = ;registry;
run
It executes all the dependency tree required by a service and return a promise that will resolve to the service itself.
registry ;
merge/clone
It allows to create a new registry, merging services of different registries:
const registry4 = registry1
Calling merge without argument, creates a clone.
getAdjList
It returns the adjacency list in the following format:
/*A ----> B| / || / || / || / || / |VV VC ----> D*/ registry;/* returns{ 'A': [], 'B': ['A'], 'C': ['A', 'B'], 'D': ['B', 'C']}*/
missingDeps
This method returns an array of service names that are not in the registry, but are dependencies of another service. This can be useful for debugging.
getMetadata
It returns the metadata of all services:
registry;/* returns{ 'A': { name: 'A', // service name deps: [], // list of deps doc: '...', // service documentation string debugInfo: { fileName: ... // file name where service is defined line: ..., // line of code where the service is defined functionName: ..., // service function name (if defined) parentFunctionName: ..., // the function containing the service definition } }, ...}*/
Service
You can get a service from the registry with the "service" method.
const service = registry;
All the service methods returns a service instance so they can be chained.
dependsOn
It defines the dependencies of a service. It may be an array or an object:
service; service;
Using an object you can use the dependencies under different names. For example, this are equivalent:
service;service;
You can use the object like this:
service ;
provides
You can pass a function taking a dependencies object as argument, and returning a promise.
service;
You can also pass any "non function" argument:
service; // Any non function argument
Or a synchronous function:
service;
If you pass an array or more than one argument, the first arguments are used to decorate the others:
service;// is the equivalent ofservice;
doc
get/set the documentation string.
service; // returns documentation stringservice; // set the doc string
getMetadata
It returns the metadata of this service:
service;/* returns{ name: 'A', // service name deps: [], // list of deps doc: '...', // service documentation string debugInfo: { fileName: ... // file name where service is defined line: ..., // line of code where the service is defined functionName: ..., // service function name (if defined) parentFunctionName: ..., // the function containing the service definition }}*/
Registry Runner
This object runs services, keeping track of their execution.
run
This method runs one or more services:
registryRunner
by default it returns a promise but can also use a callback (using the node convention):
registryRunner
you can run multiple services using a regular expression or an array of names.
You can also pass an object with some extra dependencies to be used for this execution:
registry
The extra dependencies won't be added to the original registry.
shutdown
The purpose of this method is to allow all asynchronous call to be terminated before a system shutdown. After calling this method the registry runner won't execute the "run" method anymore (It will return an exception). The method returns a promise (or uses a callback). This will be fulfilled when all previous "run" has been fulfilled of rejected.
registryRunnerregistryRunner registry registryRunner // rejected with DiogenesShutdownError
flush
Flush runs a shutdown and then restore the registry to its normal state.
Compatibility
Diogenes is written is ES6. Please transpile it for using with old browsers/node.js. Also provide a polyfill for Promises, WeakMaps and Sets.
Acknowledgements
Diogenes won't be possible without the work of many others. The inspiration came from many different patterns/libraries. In particular I want to thank you:
- architect for introducing the idea of initialising a system using the dependency injection pattern
- electrician that explored a way to wrap the code in "components" having a common interface, that is a prerequisite of having them working together