0.1.0 • Published 6 years ago

@mitmaro/dependency-manager v0.1.0

Weekly downloads
1
License
ISC
Repository
github
Last release
6 years ago

Node Dependency Manager

Dependency Status Build Status Coverage Status NPM version GitHub license Known Vulnerabilities

Motivation

Wiring together dependant pieces of a JavaScript project can be difficult and tedious. This library aims to provide a mechanism to manage and provide the dependant parts of a JavaScript project.

Install

npm install --save @mitmaro/dependency-manager

Documentation

Usage

Creating an instance

Creating a dependency manager instance is pretty straight forward.

const {DependencyManager} = require('@mitmaro/dependancy-manager');
const dependencyManager = new DependencyManager({
    // options
});

Options

NameTypeDescriptionDefault
argumentInjectionbooleanInject dependencies as arguments instead as an objectfalse
snakeCasebooleanUse snake case instead of camel case for creating identifiersfalse
argumentInjection

By default the dependencies are injected as an object with keys based on the dependency name. This options will instead provide the dependencies as arguments to the factory function based on the order of the dependency list. The option snakeCase has no visible effect when using this option.

const serviceFactory = (config, database) => {
	// ...
}
dependencyManager.register('service', serviceFactory, ['config', 'database']);
snakeCase

Generally dependencies names are converted to the parameter name in the dependency object using camel case, by providing this option they will instead be snake cased.

const serviceFactory = ({database_connection}) => {
	// ...
}
dependencyManager.register('service', serviceFactory, ['DatabaseConnection']);

Set a static value

The most simple type of dependency is a constant static value. You can provide a constant value using the set method.

dependencyManager.set('configuration', {
    databaseConnectionUrl: 'postgresql://localhost:5432/',
});

Static values should only exist for truly static values. If you are using static with a result of a function call or with an instance created with new you should be using a factory function.

Register a Factory functions

The way to define an item, that may requires other dependencies, is with a factory function. The factory function takes the optional dependencies and returns an instance of the dependency. For example to create a database connection that requires a database connection configuration you would have the factory function:

// `configuration` is the constant value set in the previous section
const databaseFactory = ({configuration, database}) => database.connect(configuration.databaseConnectionUrl);

const serviceFactory = ({database}) => (id) => {
    return database.query('SELECT * FROM foo WHERE id = ${id}', {id});
};

dependencyManager.register('database', databaseFactory, ['config']);
dependencyManager.register('service', serviceFactory, ['database']);

Aliasing

Sometimes it is useful to provide an alternative name, an alias, to a dependency. This works with constant and factory dependencies. Aliases follow the same naming rules as dependencies.

dependencyManager.alias('database', 'db');
dependencyManager.alias('configuration', 'config');

Naming

A dependency name must only contain alphanumeric characters, $, _ and - characters; and it must not must begin with a number. The name will be converted to camel cases (or snake case if the option is selected) when it is injected into a factory function with object injection.

Namespaces

Most large projects will have dependencies that can be categorized. For example there could be a number of libraries, utilities or services. In these cases an optional namespace can be prepended to the name to group these common dependencies. When registering or setting a dependency use he naming format of namespace:name. A namespaced dependency has a slightly different object injection format of nameNamespace with namespace following the same rules defined in above for dependency names.

const serviceFactory = ({databaseLib}) => (id) => {
    return databaseLib.query('SELECT * FROM foo WHERE id = ${id}', {id});
};

dependencyManager.register('lib:database', databaseFactory, ['config']);
dependencyManager.register('service', serviceFactory, ['lib:database']);

Loading

Once all the dependencies are registered, the next step is to load the dependencies. This is achieved using the load method and it's usage is pretty straight forward:

dependencyManager.load()
    .then(() => {
        console.log('Dependencies are all loaded');
        const service = depdenencyManager.get('service');
        return service('my_id');
    })
    .catch((err) => {
        console.error('An error occurred while loading dependencies');
        console.error(err);
    });

Best Practices

Be immutable

When ever possible the injected dependencies should be immutable. in that once the dependency is set, it should not be possible to change that dependency. For example:

function myService({dependency}) {
    let myDependency = dependency;
    return {
        // this is a bad function
        setDependency(newDependency) {
            myDependency = newDependency;
        }
    }
}

Inject direct dependencies only

While it might be tempting, avoid using the dependencies of a dependencies as this creates a tight coupling. For example do not do:

function myService({dependency}) {
    let mySubDependency = dependency.subDependency;
    // ...
}

The alternative is to instead directly inject the dependency:

function myService({dependency, subDependency}) {
    // ...
}

Resolving cyclic dependencies

Sometimes you will have one dependency, say FooService that has a dependency on BarService. BarService in turn has a direct, or indirect, dependency on FooService. This will result in an error:

Cycle detected in dependencies: FooService > BarService > FooService

Assuming the cycle is not an error, there are a couple ways to resolve the cycle.

Remove the cycle

Most often a cycle is a sign that the a dependency is performing an action that it should not. To resolve the cycle extract the dependant functionality into a separate shared dependency.

Inject a provider function

Sometimes it is not possible to remove the dependency cycle, in this case you can use a factory function to create a dependency. For example:

function createFooService({anotherDependency, barService}) {
	// ...
}

function fooServiceProvider({anotherDependency}) {
    let fooService;
    return {
        create(barService) {
            fooService = createFooService({anotherDependency, barService});
            return fooService;
        },
        get() {
            return fooService;
        }
    }
}

function createBarService({fooServiceProvider}) {
    let fooService;
    const bar = {
        callFooService() {
            fooService()
        }
    };
    fooService = fooServiceProvider.create(bar);
    return bar;
}
dependencyManager.register('fooServiceProvider', fooServiceProvider, ['anotherDependency']);
dependencyManager.register('barService', fooServiceProvider, ['fooServiceProvider']);

The service provider above creates a singleton instance of fooService, but this is optional if multiple instances of fooService are desired.

Avoid side effects

Often you will have a dependency that will make an interaction to an external system, such as opening a database connection, loading a file for writing, or starting a HTTP server. Avoid performing these, and similar, actions in a factory function. Instead create a service interface that exposes a function to start and optionally stop the action. Then call the start and stop functions as part of your application bootstrap and shutdown. For example:

function httpService({http}) {
	let httpConnection;
	const service = {
		start() {
			return http
				.start()
				.then((connection) => {
					httpConnection = connection;
				});
		},
		stop() {
			return httpConnection.end();
		}
	}
}

Development

Development is done using Node 8 and NPM 5, and tested against both Node 6 and Node 8. To get started

  • Install Node 8 from NodeJS.org or using nvm
  • Clone the repository using git clone git@github.com:MitMaro/node-dependency-manager.git
  • cd node-dependency-manager
  • Install the dependencies npm install
  • Make changes, add tests, etc.
  • Run linting and test suite using npm run test

License

This project is released under the ISC license. See LICENSE.