Last active
December 31, 2020 22:05
-
-
Save dfkaye/ce020f21c4e095bd3da5c8026b2e5e3e to your computer and use it in GitHub Desktop.
barebones dependency injection example supporting both factory functions and object specifiers
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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