Skip to content

Latest commit

 

History

History
1479 lines (1040 loc) · 36.3 KB

README.md

File metadata and controls

1479 lines (1040 loc) · 36.3 KB

hinoki

this is a release candidate for version 1.0.0. implementation and tests are complete. the documentation in this readme is not yet finished. it's already useful though. the newest version introduces massive breaking changes. if you used an earlier version of hinoki please read this new readme carefully. future changes will be documented in the changelog. future versions will use semver.

NPM Package Build Status Sauce Test Status codecov.io Dependencies

effective yet simple dependency injection and more for Node.js and browsers

Hinoki seems to be the least surprising IoC container available for Node.
I definitely do like its ascetic worldview.
Thanks a lot for this!
andrey

(web) applications are systems. systems with many parts: functions, helpers, actions, objects, data, configuration. parts depend on each other. often asynchronously in the case of Node.js. often in complex ways. in Node.js they often depend on each other asynchronously. often they depend on each other in complex ways.

such complex systems consisting of many interdependent parts can be simplified by using dependency injection.

dependency injection can simplify such systems.

dependency injection is a simple idea:
each part declares its dependencies (the other parts it needs). there is a piece of code for constructing each part. called a factory parts of the system say which other parts they need. then dependency injection makes sure they get them.

just by the name of their function parameter. the injector then hands those dependencies to the parts that need them.

the injector !!!

clojure based dependency injection. the wiring happens automatically. manual wiring boilerplate disappears. nothing is hardwired. everything is mockable. every part is easy to test in isolation.

hinoki is an extremely simple and functional approach to dependency injection. it supports usage patterns not possible with other dependency injection systems.

generalizes the concepts behind dependency injection.

you can use it for traditional dependency injection. you can also do more and exiting things

Sauce Test Status

read on [stick around] for the scenic tour.

hinoki is a bit like a map with the addition that values can depend on each other.

this the dependency graph (which values depend on each other) is controllable programmatically.
like a map hinoki manages a mapping from keys to values.
we can ask for a key and get a value.
unlike a map:
if we ask for a key that has no value, then a user-provided source function is called.
that source function doesn't return a value directly.
instead it returns a factory, which is simply a function that returns a value.
hinoki looks at the factory's parameter names, interprets them as keys, looks up their values, calls the factory with the values which returns a value.
sources dynamically extend the keyspace managed by hinoki.

you can make a value depend on other values simply by

var factories = {
  six: function(one, two, three) {
    return one + two + three;
  }
}

like a map we can use hinoki as a building block in solving a whole variety of problems.

we use it in the large to structure entire web applications.

we use it in the small to tame complex asynchronous I/O flows (you know... those where you do some async I/O that depends on three other async results and is needed by two other...)

hinoki is an abstract and flexible tool. when applied to the right problems it can reduce incidental complexity and boilerplate, make code more testable, easier to understand, simpler, less complex and more elegant.

reading and understanding the rest of this readme should take less than 10 minutes.
its goal is to make you thoroughly understand hinoki's interface and core concepts.
hopefully enabling you to extrapolate to solve your own problems.

hinoki is inspired by prismatic's fantastic graph.

hinoki takes its name from the hinoki cypress, a tree that only grows in japan and is the preferred wood for building palaces, temples and shrines.
we hope hinoki becomes the building material for your digital palaces too !

get it

npm install hinoki
var hinoki = require('hinoki');
bower install hinoki

lib/hinoki.js supports AMD. if AMD is not available it sets the global variable hinoki.

keys and values

like a map hinoki manages a mapping from keys to values.

think of the values as the parts of your system.
think of the keys as the names for those parts.

like a map we can ask for a value that is associated with a given key:

var source = function() {};

var lifetime = {
  one: 1,
  two: 2,
  three: 3,
};

var key = 'three';

hinoki(source, lifetime, key).then(function(value) {
  assert(value === 3);
});

the first argument is the source. more on that in a bit. here it does nothing. it's not optional don't worry about it for now. we'll come to that in a bit.

the second argument is a lifetime.
a lifetime is a plain-old-javascript-object that maps keys to values.
lifetimes store values.
we'll learn more about lifetimes later.

the third argument is the key we want to get the value of.

if we ask for a key and a value is present for that key in the lifetime then hinoki will return a promise that resolves exactly to that value !

hinoki always returns a promise !

that code not very useful. we could have used lifetime.three directly.
we'll get to the useful stuff in a bit !


we can also look up the values for multiple keys in multiple lifetimes at once:

var lifetimes = [
  {one: 1},
  {one: 'one'},
  {two: 2, three: 3},
  {two: 'two'},
];

hinoki(function() {}, lifetimes, ['three', 'one', 'two'])
  .spread(function(a, b, c) {
    assert(b === 1);
    assert(c === 2);
    assert(a === 3);
  });

the value is always returned from the first lifetime having the key !

multiple lifetimes are really useful. more on that later.


we can even pass in a function as the third argument:

var lifetimes = [
  {one: 1},
  {two: 2},
  {three: 3},
];

hinoki(function() {}, lifetimes, function(two, one, three) {
  assert(one === 1);
  assert(two === 2);
  assert(three === 3);
  return two + three;
}).then(function(value) {
  assert(value === 5);
})

hinoki has just parsed the keys from the function parameter names and called the function with the values (associated with those keys) as arguments.

in this case hinoki returns a promise that resolves to the value (or promise) returned by the function.

that's an improvement but still not really what hinoki is about.

let's get to that now !

sources and factories

what if we ask for a value that is not present in the lifetime(s) ?

var lifetime = {
  one: 1,
  two: 2,
  three: 3,
};

hinoki(function() {}, lifetime, 'four').then(function(value) {
  /* this code is never reached */
});

the promise is rejected with an error:

Unhandled rejection NotFoundError: neither value nor factory found for `four` in path `four`

if there is no value for the key in any of the lifetimes hinoki calls the source function with the key !

think of source as a fallback on missing key value mappings.

the source is not supposed to return a value directly. instead the source is supposed to return a factory or null.

a factory is simply a function that returns a value.

returning null (undefined is fine too) signals hinoki that the source can't return a factory that can make a value for that key.

sources make factories.
factories make values.

factories declare dependencies through their parameter names:
the parameter names of a factory function are interpreted as keys. hinoki will get values for those keys and call the factory with them as arguments.

let's see an example:

var lifetime = {
  one: 1,
  two: 2,
  three: 3,
};

var source = function(key) {
  if (key === 'five') {
    return function(one, four) {
      return one + four;
    };
  }
  if (key === 'four') {
    return function(one, three) {
      return Promise.resolve(one + three);
    };
  }
};

hinoki(source, lifetime, 'five').then(function(value) {
  assert(value === 5);
  assert(lifetime.five === 5);
  assert(lifetime.four === 4);
});

there's a lot going on here. let's break it down. you'll understand most of hinoki afterwards:

we want the value for the key 'five'.
hinoki immediately returns a promise. it will resolve that promise with the value later.
there is no key 'five' in the lifetime so hinoki calls our source function with the argument 'five'.
source returns the factory function for 'five'.
the factory for 'five' has parameter names 'one' and 'four'.
hinoki calls itself to get the values for 'one' and 'four' such that it can call the factory with those values.
'one' is easy. it's already in the lifetime.
but there is no key 'four' in the lifetime therefore hinoki calls our source function again with the argument 'four'.
source returns the factory function for 'four'.
the factory for 'four' has parameters 'one' and 'three'.
hinoki calls itself to get the values for 'one' and 'three'.
fortunately values for both 'one' and 'three' are already in the lifetime.
hinoki can now call the factory for 'four' with arguments 1 and 3.
the factory for 'four' returns a promise.

when a factory returns a promise hinoki must naturally wait for the promise to be resolved before calling any other factories that depend on that value !

at some point the promise for 'four' resolves to 4.
hinoki can now continue making everything that depends on the value for 'four':

first hinoki sets lifetime.four = 4.

values returned from factories are stored/cached in the lifetime !
what if we have multiple lifetimes? the answer is very useful and worth its own section.

now hinoki can call the factory for 'five' with arguments 1 and 4.
ince the factory for 'five' doesn't return a promise hinoki doesn't have to wait.
hinoki sets lifetime.five = 5.
remember that promise hinoki has returned immediately ? now that we have the value for key 'five' hinoki resolves it with 5.

that's it for the breakdown.
you should now have a pretty good idea what hinoki does.
keep in mind that this scales to any number of keys, values, factories, lifetimes and dependencies !

having to add an if-clause in the source for every factory is not very convenient.
fortunately there's much more to sources. read on !

sources in depth

the first argument passed to hinoki is always interpreted as the source and passed to hinoki.source internally.

hinoki.source takes either an object, a string, an array or a function.
hinoki.source always returns a source function.

when hinoki.source is called with a function argument its simply returned.

objects

if an object mapping keys to the factories for those keys is passed to hinoki.source it is wrapped in a source function that simply looks up the key in the object:

var lifetime = {
  one: 1,
  two: 2,
  three: 3,
};

var factories = {
  five: function(on, three) {
    return one + four;
  },
  four: function(one, three) {
    return Promise.resolve(one + three);
  },
};

hinoki(factories, lifetime, 'five').then(function(value) {
  assert(value === 5);
  assert(lifetime.five === 5);
  assert(lifetime.four === 4);
});

much more readable and maintainable than all those if-clauses we had before.

strings

if a string is passed to hinoki.source it is interpreted as a filepath.

if the filepath points to a .js or .coffee file hinoki.source will require it and return a source function that looks up keys in the module.exports returned by the require.

if the filepath points to a folder hinoki.source will require all .js and .coffee files in that folder recursively. other files are ignored. all module.exports returned by the requires are merged into one object. a source function is returned that looks up keys in that object.

it is very easy to load factories from files that simply export factories this way !

sources can also be strings. in that case hinoki interprets them as filenames to require

this means that you can just drop your factories as exports pull them all in and wire them up.

you could have all your factories as exports in a number of

your factories could then depend on other factories exported by any other file in that directory.

arrays

sources compose:
if an array is passed to hinoki.source it is interpreted as an array of potential sources.

this section needs work

generator functions

this section needs work

decorator functions

this section needs work

lifetimes in depth

this section needs work

lifetimes store values.

why multiple lifetimes ?

because values

there's only one application

but there are many requests

there are many events

many lifetimes

but you still want

let's see an example:

request, response

fragments is with this idea at its core.

factories in depth

this section needs work

changelog

future changes will be documented here