Skip to content

Instantly share code, notes, and snippets.

@dfkaye
Last active December 31, 2020 22:05
Show Gist options
  • Save dfkaye/ce020f21c4e095bd3da5c8026b2e5e3e to your computer and use it in GitHub Desktop.
Save dfkaye/ce020f21c4e095bd3da5c8026b2e5e3e to your computer and use it in GitHub Desktop.
barebones dependency injection example supporting both factory functions and object specifiers
// 10 December 2020
// This is a simple example of binding cyclic dependencies into an API object.
// It is not intended as a framework or even a library for re-use, but only to
// illustrate the pattern that emerges when we use factory functions to create
// objects (or return other functions). It allows us to create them independently,
// so they don't need to import each other, and to bind them at a later time.
// There is still a testing story to be filled out which I defer for now.
// 28 December 2020 testing update:
// 1. Added "mock" without dependencies to verify 0-dep works.
// 2. Verified we can extend a missing dependency after creation.
// We'll use `define()` as the injection function (the name is subject to revision).
// The param, factories, is an object containing factory functions.
// Each factory accepts any number of dependencies.
// Each factory returns an object containing methods.
// `define()` returns an object containing all the returned factory objects.
function define(factories) {
var api = {/* This is what will return. */}
// Non-object coercion to an object so we don't iterate keys on null, undefined, or worse.
factories = Object(factories)
Object.keys(factories).forEach(key => {
var F = factories[key];
// Add the slot for the factory if it hasn't been defined on the api yet.
var slot = key in api ? api[key] : (api[key] = {})
if (typeof F != 'function') {
// If the factory is not a function, map its keys onto the slot, and stop processing F.
return Object.assign(slot, F)
}
// This is a trick that Angular v1 and require.js use to create an array of
// objects mapped to each param name, so that function a(b, c, d) {...} will
// be parsed to an array, as in `[ api["b"], api["c"], api["d"] ]`.
var dependencies = F.toString()
// match "b, c, d" from "function a(b, c, d)"
.match(/[\(]([^\)]*)[\)]/)[1]
// split "b, c, d" into ["b", " c", " d"]
.split(",")
// map ["b", " c", " d"] into [api["b"], api["c"], api["d"]]
.map(k => {
var n = k.trim()
// Add a slot for the dependency if it hasn't been defined on the api yet.
n in api || (api[n] = {})
return api[n]
})
// Apply the factory function with its array of dependencies.
Object.assign(slot, F.apply(void 0, dependencies))
})
return api
}
// Two example factories, a and b, depend on each other (thus we have a cycle).
// They return the same kind of send and receive API containing closures over
// their function name. The send() methods have access to their dependencies'
// APIs.
function a(b) {
var name = a.name;
return {
send() { b.receive(`${name} says hi.`) },
receive(msg) { console.log(`${msg} received by ${name}.`) }
}
}
function b(a, c, d) {
var name = b.name;
return {
send() { a.receive(`${name} says hi.`) },
receive(msg) { console.log(`${msg} received by ${name}.`) },
test() { c.test(), d.test() }
}
}
// 28 December 2020
// Mock dependency - use it to mock the c slot that b requires, to check that
// that "c" isn't a symbol already in the namespace.
var C = function () {
return {
test() {
console.log("C.test() works")
}
}
}
// 31 December 2020
// Mock dependency as an object.
// When a factory is an object, its methods are already closed; that is,
// if D.test() refers to a.receive(), the reference to `a` will fail because at
// this point (assuming each factory is defined in separate modules), `a` is undefined.
var D = {
test() { console.log("D.test() works") }
}
// At the point in the code base where we'd create the "app", we would import each part:
// import a
// import b
// import c or mock C
// import d or mock D
// import define
// Then use define() to create the binding between dependencies a and b (and c and d).
var sam = define({ a, b, c: C, d: D })
sam.a.send()
sam.b.send()
// test the mocks
sam.b.test()
// Should print:
// a says hi. received by b.
// b says hi. received by a.
// C.test() works
// D.test() works
// 28 December 2020 - verify extension works when dependency not provided.
var o = define({ A: a })
// Creates o with dependencies "A" (not "a"), and an empty "b" object.
// o {
// A { name, send, receive },
// b { }
// }
try { o.A.send() }
catch (err) { console.log(`Should throw an error: ${err}`) }
// Now, extend the o.b interface with a custom receive() method, and try again.
o.b.receive = function (s) { console.log(`Received by B: "${s}"`) }
o.A.send()
// Should print:
// Received by B: "a says hi."
// Now we can exercise SAM pattern steps independently.
/*
var test = define({ model }) // => { model, state }
test.state.change = function(data) { this.data = data }
model.propose({ ... })
var { data } = test.state
expect(data.displayValue).to.equal(0)
*/
// Et cetera.
// :)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment