Giter Club home page Giter Club logo

depject's Introduction

depject

simplest dependency injection

Installation

$ npm install --save depject

philosophy

A module exposes features to be used by other modules, and may also depend on features provided by other modules. Any module system can do that. In the node module system, modules declare exactly which modules they depend on. That works well when the module does a very well defined task, that can be abstractly solved. In other words, it works well when the module solves a technical problem.

But it doesn't work so well when the module just represents an opinion. Developer tools seem to be dominated by technical problems, but user applications seem to be dominated by opinions. There are many different ways something could be implemented, no objectively optimal solution, and loads of pretty good ones.

The contemporary best practice is to embrace that, and create software that has strong opinions. That takes a strong leader to make decisions, compromises be dammed. I am building a p2p system, and have gone to considerable effort to create a decentralized protocol. But then, if we have a user interface with strong opinions, then that recentralizes development.

My strong opinion is to reject strong opinions. depject is a strategy to deopinionate software. It should be easy to change any particular opinion.

Another way to look at this, is the goal is to make pull-requests that merge easily. with node's module system, a dependant module must declare exactly which modules they depend on. That means, to add a feature, you need to add a new file implementing it, and also update files that use that.

To contrast, in depject if that feature is the same shape as one already existing, you only need to add that file. This means you can add merge two new features, with out a conflict.

patterns

first - use the first module that has an opinion about a thing.

Say we have a system with multiple types of messages. Each type has a renderer. We want to call all the renderers, and get the first one that knows how to handle that value.

map - get each module's opinion about a thing.

Say we have a menu that is actions which may be performed on a thing. We map the modules over that thing, and add all returned items to a menu.

reduce - compose each modules opinion about a thing into one opinion.

We might want to allow other modules to decorate the value given by our module

example

Using first

const combine = require('depject')

const cats = {
  gives: 'animalSound',
  create: () => (type) => {
    if(type !== 'cat') return
    return 'Meow'
  }
}

const dogs = {
  gives: 'animalSound',
  create: () => (type) => {
    if(type !== 'dog') return
    return 'Woof'
  }
}

const speak = {
  needs: {animalSound: 'first'},
  gives: 'speak',
  create: (api) => api.animalSound
}

const sockets = combine([cats, dogs, speak])

const mySpeak = sockets.speak[0]

console.log(mySpeak('dog'))
//Woof

Using map

const combine = require('depject')

const cats = {
  gives: 'name',
  create: () => () => 'Fluffy'
}

const dogs = {
  gives: 'name',
  create: () => () => 'Rex'
}

const animals = {
  needs: {name: 'map'},
  gives: 'animals',
  create: (api) => api.name
}

var sockets = combine([cats, dogs, animals])

var myAnimals = sockets.animals[0]

console.log(myAnimals())
//['Fluffy', 'Rex']

api

modules

Each module is an object which exposes {needs, gives, create} properties. needs and gives describe the module features that this module requires, and exports.

needs is a map of names to types. {<name> : "map"|"first"|"reduce"}

gives Is a string name of it's export, or if there are multiple exports an object where each key is a name {<name>: true,...}.

create Is a function that is called with an object connected to modules which provide the needs and must return a value which provides the gives or an object with keys that match what the module gives.

combine

Actually connect all the modules together! Takes an array of modules, resolves dependencies and injects them into each module.

combine([modules...])

This will return an array object of arrays of exports.

exporting more than one thing from a module

const cats = {
  gives: {name: true, animalSound: true},
  create: () => ({
    name: () => 'Fluffy',
    animalSound: (type) => {
      if(type !== 'cat') return
      return 'Meow'
    }
  })
}

requiring more than one thing into a module

const animalSounds = {
  needs: {name: 'map', animalSound: 'first'}
}

deeply nested modules

It's possible to pass deeply nested modules to combine eg:

const modules = {
  a: {
    b: {
      c: {
        gives: 'yes',
        create: function () {
          return function () {
            return true
          }
        }
      }
    },
    d: {
      e: {
        needs: {
          yes: 'first'
        },
        gives: 'no',
        create: function (api) {
          return function () {
            return !api.yes()
          }
        }
      }
    }
  }
}

const api = combine(modules)

design questions

Should there be a way to create a routed plugin? i.e. check a field and call a specific plugin directly?

How does this interact with interfaces provided remotely? i.e. muxrpc?

License

MIT © Dominic Tarr

depject's People

Contributors

ahdinosaur avatar dominictarr avatar hackergrrl avatar mixmix avatar pietgeursen avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

depject's Issues

combinatorials

lyk dis:

combo = combine([a,b,c])
comboDelux = combine([combo,e,f,g])

has anyone used reduce yet?

has anyone actually used a reduce plug yet? I originially put it in become it seemed sort of useful, but I havn't actually used it. If someone has, can you write about how it went, and link to code?

Using reduce for decorators

I'd just like a sanity check if anybody has time - is this a reasonable pattern? Does it seem overwrought or about right? I have a gut feeling that maybe the defaultDecorator module isn't strictly necessary but can't get my head around how it would work without it (I'm easily confused by currying multiple functions, my brain doesn't like it for some reason)

It's a contrived example but not totally dissimilar in intent to the existing use cases I talked about in #25

Additionally, can anyone point me at examples of other people doing similar things?

const addDefault = ( arr, child ) => arr.push( child )

const addEnforceChild = add => ( arr, child ) => {
  if( child === undefined )
    throw new Error( 'No child!' )

  return add( arr, child )
}

const addEnforceLength = add => ( arr, child ) => {
  if( arr.length > 1 )
    throw new Error( 'Too many items!' )

  return add( arr, child )
}

const addModule = {
  needs: { decorateAdd: 'reduce' },
  gives: 'add',
  create: fn => ( arr, child ) => fn.decorateAdd( addDefault )( arr, child )
}

const defaultDecorator = {
  gives: 'decorateAdd',
  create: () => add => add
}

const enforceChildDecorator = {
  gives: 'decorateAdd',
  create: () => addEnforceChild
}

const enforceLengthDecorator = {
  gives: 'decorateAdd',
  create: () => addEnforceLength
}

examples make no sense

the example should resemble a case that also demonstrates why you'd want to use depject. if you just wanted to get the sounds that cats and dogs make, there are simpler ways to do it than use this library...
not sure what that would be but am thinking about depject todo mvc?

dynamic modules

sometimes it seems necessary to have a dependency you didn't know that you had, or have something like a router, which different add handlers to.

I showed depject to @nkrn and he tried it and wrote this about his experience https://github.com/nrkn/classical-vs-depject/blob/master/readme.md

under "Cons" he mentions:

"I can't figure out how once you've called combine you can then plug more modules into your sockets object post the fact - this is probably not so much a con as a lack of comprehension on my part!"

So, opening this issue as a place to discuss this question. I have had this problem recently, although I wasn't using depject at the time. (I was using secret-stack's [crappy] plugin system [which led to depject])

Here, two "plugin" apis collide. flume takes a simple interface and then wraps it a bit (taking database views and adding a thing to delay the request to the view to ensure read consistency) but the secret-stack plugins are supposed to export the apis that it creates. I can't have a flume sbot plugin that doesn't know what it's gonna export, and other plugins that know what they want to export but expect that to be exported by another thing.

But then i figured out that I didn't actually need it!
https://github.com/ssbc/scuttlebot/blob/flume/plugins/friends.js#L35

So what I did, was just have the flume method return the wrapped view, _flumeUse (not how you'd normally use flume) and then the plugin (in this case, friends, which shows who follows who, etc) just exports it's own api, which it happens to initialize through another plugin.

The only limitation here is that the since the friends plugin has to call another plugin during initialization (create method in depject) that you can't have cyclic dependencies (but you should avoid that anyway!)

I'm sure there are other ways you might need dynamic modules though, this is just one example.

idea: depject app as script tags

Add a simple interface to depject so that instead of calling combine(...) you can expose(module) (or something like that) and then just add the various layers as script tags, or concatenate them and it should just work. (JavaScript modules just went full circle)

this means we can add ship depject apps over ssb blobs, and then curate multiple layers, concatenating them, and everything will usually just work. or you could have a configuration that says, load this, load that as in %JDTx0jRXX8HX4E0QLyM4fsZUdvsSk8QkYRDLSbGicW0=.sha256

[apply] be able to pass in custom apply function

use case: in inu/entry, i need to be able to apply the modules in a custom way. at the moment this happens on the sockets after combine, but i reckon it'd be better if this happened as a normal module.

proposal:

const reduce = require('./lib/reduce')
const many = require('./lib/many')

exports.gives = nest('inu.store')
exports.needs = nest('inu', {
  init: reduce,
  update: reduce,
  run: many,
  view: 'first',
  enhancer: 'reduce'
})
exports.create = (api) {
  return nest('inu.store', () => {
    return api.inu.enhancer({
      init: api.inu.init,
      update: api.inu.update,
      run: api.inu.run,
      view: api.inu.view
    })
  })
}

A needed module that returns a number returns undefined if the number is zero

It happens because apply.js/first is checking for a falsey value.

const decrement = {
  gives: 'decrement',
  create: () => i => i - 1
}

const decrementOne = {
  gives: 'decrementOne',
  needs: {decrement: 'first'},
  create: api => () => api.decrement(1)
}

const sockets = combine([decrement,decrementOne])

console.log(sockets.decrement[0](1)) // 0 - correct
console.log(sockets.decrementOne[0]()) // undefined :(

In apply.js:

{
  // ...
  first: function (funs) {
    return function (value) {
      if (!funs.length) throw new Error('depject.first: no functions available to take first')
      var args = [].slice.call(arguments)
      for (var i = 0; i < funs.length; i++) {
        var _value = funs[i].apply(this, args)
        /*
          Oh noes :(
        */
        if (_value) return _value
      }
    }
  }
}

Now, I understand the intent behind first checking for a falsey value. I have a fork that corrects the behaviour and all the tests still pass, but that doesn't necessarily mean that people might be (ugh) returning 0 deliberately in the wild? So a fix will break it for them. Though, my thought is kinda, those people are bad and they deserve for their code to break :P

gives and needs sugar

with using nested plugs, sometimes the gives and needs objects can be somewhat annoying.

here's some possible ways to sugar this cat:

  1. give string
exports.gives = 'cats.actions.create'
  1. gives object
exports.gives = {
  'cats.actions.create': true
}
  1. needs object
exports.needs = {
  'cats.actions.create': 'first'
}
  1. gives array?
exports.gives = [
  'cats.actions.create'
]
  1. needs array?!
exports.needs = [
  ['cats.actions.create', 'first']
]

of course these could be just helper modules, but opening a discussion to include here.

/cc @mixmix @pietgeursen @mmckegg

better error message when created plug is not a function

given a module that returns an object rather than a function:

module.exports = {
  gives: 'styles',
  create: () => ({
    colors: {
      primary: 'green'
    }
  })
}
const html = require('bel')

module.exports = {
  gives: 'button',
  needs: { styles: 'first' },
  create: (api) => () => html`
    <button
      style='background-color: ${api.styles().colors.primary}'
    >click me!<button>
  `)
}

currently confusing error message:

TypeError: funs[i].apply is not a function
    at Object.styles (/home/dinosaur/repos/enspiral-root-systems/cobuy/node_modules/catstack/node_modules/depject/apply.js:13:30)
    at module.exports.create (/home/dinosaur/repos/enspiral-root-systems/cobuy/orders/element/orderPage.js:18:47)
    at Object.Module.module.create (/home/dinosaur/repos/enspiral-root-systems/cobuy/node_modules/catstack/lib/module.js:12:23)
    at combine (/home/dinosaur/repos/enspiral-root-systems/cobuy/node_modules/catstack/node_modules/depject/index.js:18:24)
    at startServer (/home/dinosaur/repos/enspiral-root-systems/cobuy/node_modules/catstack/server.js:10:19)
    at Object.command (/home/dinosaur/repos/enspiral-root-systems/cobuy/node_modules/catstack/server/command.js:16:7)
    at doCb (/home/dinosaur/repos/enspiral-root-systems/cobuy/node_modules/subcommand/index.js:63:19)
    at _combinedTickCallback (internal/process/next_tick.js:67:7)
    at process._tickCallback (internal/process/next_tick.js:98:9)
    at Function.Module.runMain (module.js:577:11)

idea: depject image manipulation app based around canvas

I think a modular app for doing stuff with canvas could be a really nice example app for depject.
image manipulation is a natural fit for modular stuff (much like music software is) there are generators and effects and transforms etc.

it could all be based around the canvas, because you can do pretty much anything with the canvas. If it generated an ecosystem of scriptable image transforms that would be very useful for a range of things, and you could run from scripts either with a headless electron, or via node-canvas (great if you can get it to build!)

but if it also had a UI that you ran inside the electron or the browser then you'd have a pretty powerful demonstration of depject.

should `combine` and `first` order be reversed?

my thinking here is that you tend to load modules in layers, from least specific ("core") towards userland and customizations.

combine(
  core,
  userland,
  customizations
)

that allows a customization to override defaults provided by core, by providing a module at a matching path to core, which overrides it.

but on the other hand, first is also really about overriding, but more granularly, because you can override only on a specific call, if the arguments matched some filter, so really we want to call the userland customizations first.

hmm, in patchbay we just call combine with the layers the other way around. https://github.com/ssbc/patchbay/blob/master/index.js#L4-L9 which has the effect I describe here.

[apply] super / extend / around

when you want to extend the functionality of an existing module, using the existing module in your own module.

exports.gives = 'say'
exports.needs = { say: 'super' }
exports.create = (api) => (text) => {
  return api.say(text.toUpperCase())
}

because you might want to 'decorate' an existing module that doesn't already have a reduce chain set up.

/cc @mmckegg @mixmix

overriding give/needs on a specific module

in patchbay currently, the styles are pretty much hardcoded in, which makes it difficult to override them. I've made some suggestions here: ssbc/patchbay#86

Another approach, is should there be a way to disable specific needs/gives on a specific module?
currently, each module exports a plug that provides css for that plug. However, that css is really the epotimy of what I call "opinions"... would it be useful to somehow disable the gives of a specific module, that way you could cancel the gives: {css : true} on each module, and get an unstyled module.

I can also imagine some other cases, where say, I have a module that views a sort of page, but also adds actions, and I want to keep the view but disable the actions. So, in that case I could disable the action, but not the view.

I'm not sure what it would look like, just yet, but it could be implemented as a function that you pass the module too.

combine(
 ....,
 override(modules, {gives: {css: false}}),
  ...
)

that would just disable the gives.css for that module, which would prevent any further down modules from using that.

module terminology

i think it's confusing that we often use the word "module" to express multiple distinct concepts:

  1. a file exporting an object with { needs, gives, create }
  2. a function needed or given at a path
  3. an array or nested object of #1 passed into combine(...)
  4. a nested object returned from combine(...) where each key is a path to some #2 and value is array of any #2 created

i'd like to come to agreement on our terminology so our code, documentation, and errors can be more clear.

personally, i think we should call #1 a "module definition", #2 a "module", #3 a "module set", and #4 a something (was "sockets", now "combined modules").

thoughts?

asynchronous create functions?

Is there a recommended pattern for implementing asynchronous create functions in depject? I'd like to be able to use depject as a somewhat foundational layer of a bit of software I'm making, but some of the services I want to provide (like a database abstraction) require asynchronous initialization.

I'm relatively new to it, but on the surface of it, it seems like it wouldn't be too hard to move:

var given = module.create(needed)
assertGiven(module.gives, given, key)
addGivenToCombined(given, combinedModules, module)

To something like:

var given = module.create(needed)

if (isPromise(given)) {
  given.then(function(givenModule) {
    assertGiven(...)
    addGivenToCombined(...)
  });
} else {
  assertGiven(...)
  addGivenToCombined(...)
}

But that's obviously a naive hack and I'd need to think through the proper control flow. Would a PR that adds this functionality be welcome, or does it go against the grain? Are callbacks or promises referred?

a silly idea: frameworks and plug paths as files paths

within the patch* ecosystem (patchcore, patchwork, patchbay, patch-gatherings), it feels like a pattern is emerging (similar to how i did catstack):

  • every module gives one plug
  • the path of every plug is based on the path of the module file
  • a "plugin" is a collection of plugs (so module file paths are relative to the plugin root)

meanwhile, i'm wondering how we apply the depject ideas to a more palatable framework module system (appeals to developers outside of the mad science karass). so i've been thinking what each type of needs does in practice:

  • first: get me the most specific plug at a given path
  • map: get me all the modules at a given path
  • reduce: get me the "previous" module at a given path (so i can decorate it)

i wonder, what if instead of this:

const nest = require('depnest')

exports.gives = nest('gathering.async.create')

exports.needs = nest({
  'sbot.async.publish': 'first'
})

exports.create = function (api) {
  return nest('gathering.async.create', function(data, cb) {
    api.sbot.async.publish({type: 'gathering'}, cb) 
  })
} 

we had this:

const publish = dep.get('sbot/async/publish')

module.exports = function (data, cb) {
  api.sbot.async.publish({type: 'gathering'}, cb) 
}

and map is something like

const actions = dep.map('message/html/action')

notes:

  • reduce is made possible with: when you are at the same path, get returns the "previous" plug.
  • if you want to return multiple of the same path, you can have many files at a sub path. (see message/html/render).

an assumption here is that the modules only make sense in the context of the "framework", but honestly i think that's how depject works in practice anyways (the depject modules need to be combined in order to run).

anyways, a silly idea. ❤️

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.