Using a Proxy to Discover Dependencies

Awilix can use a Proxy to dynamically discover an javascript object's dependencies.

Inversion of control (IoC) containers allow you to register objects with them so that you can later ask them to supply you with one supplied with all of its dependencies. Different container implementation use different techniques to determine an object's dependencies. Javascript's Proxy object offers an elegant alternative to the, in my opinion, clunky options that we've seen in libraries like AngularJS v1.

Forced to manually declare dependencies

In strongly typed languages like C# where reflection is well supported, the container can discover dependencies itself with no change from the user e.g. Ninject:

class MyService
{
    public MyService(IFoo foo, Bar bar) { ... }
}
// That's it, no change to the class required!

Unfortunately in languages where argument type reflection is not available (like Javascript) we have needed to declare the dependencies explicitly so that the container could find it.

For example AngularJS v1 would look for them on a special $inject property:

function MyService (foo, bar) { ... }
MyService.$inject = ['foo', 'bar'];
container.service('MyService', MyService); // Reads dependencies from `$inject`

Or if you didn't like that special property then there has been the option to declare the dependencies inline during registration e.g. BottleJS or AngularJS v1:

function MyService (foo, bar) { ... }
container.service('MyService', MyService, 'foo', 'bar')

Annotations offer another mechanism to manually declare dependencies but all of these suffer from the same duplication problem.

Parsing the function text to look for the names of the arguments is a possible approach too but code minification defeats that technique so it doesn't feel like a viable option in the browser.

Use a Proxy!

Awilix is a NodeJS IoC container that has the option to use a proxy to get back to the automatic dependency detection without using reflection.

function MyService ({ foo, bar }) { ... }
awilixContainer.registerFunction({ myService: MyService });
// How does it know the dependencies?!

Did you spot the change to the MyService function?

It now expects a single argument containing all its dependencies, which it destructures, rather than each dependency being passed as a separate argument. That one design constraint allows Awilix to do the magic; it passes its own special proxy as the argument to anything it resolves. When the object accesses a named property of the proxy, Awilix knows that the object is asking for that dependency and can resolve it!

Supplying all dependencies as a single argument does have its drawbacks, such as breaking functional composition and preventing partial application, but I find the carrot of automatic dependency detection quite appealing.

Published on: 17 Nov 2017