首页

Reflections on Dependency Injection

此文由 ai 翻译自中文版 对于依赖注入的思考-二
In a previous article titled Suddenly Realized Why Dependency Injection Is Necessary, I received a lot of feedback that pointed out significant flaws in my understanding. After some more study, I realized that the issue I was trying to address is not so much about Dependency Injection itself, but is closer to a concept known as Algebraic Effects. Both Dependency Injection and Algebraic Effects aim to solve engineering challenges in programming, such as replacing function implementations without modifying old code.

Example of Dependency Injection:

javascript
function baseFn_A() {} function baseFn_B() {} function higherFn_B() { baseFn_A(); // call baseFn_A in higherFn_B } function higherFn_C() { higherFn_B(); // call higherFn_B }
The idea is that if I want to create a new function higherFn_C2​ that differs from higherFn_C​ only in that it calls baseFn_B​ instead of baseFn_A​, Dependency Injection allows me to achieve this without changing much of the existing code.

A Key Insight on Context Propagation:

I recently came up with an idea where context is passed using a variable named ctx​. It starts as a global ctx​, and then branches into a tree for each module and function, where the context is derived from its parent. This is similar to implementing Algebraic Effects and Dependency Injection, as it allows for easy runtime replacement of context variables like replacing a closure in code.

Problems to Solve:

Changing logic without modifying old code: The challenge is to alter logic without impacting existing code.

Solutions Explored:

1.
Passing Parameters:
A simple method is passing functions as parameters, enabling logic changes without altering existing functions.
javascript
function baseFn_A(op) { return op(); } function higherFn_B(op) { baseFn_A(op); }
This is a straightforward approach to Dependency Injection but becomes cumbersome as the number of parameters increases.
javascript
function baseFn_A(op, op2, op3) { return [op(), op2(), op3()]; } function higherFn_B(op, op2, op3) { baseFn_A(op, op2, op3); }
Ideally, you want a way to modify only specific parameters without affecting others.
javascript
function baseFn_A() { const { op, op2, op3 } = requireFn(); return [op(), op2(), op3()]; } function higherFn_B() { setRequireFn({ op: () => {} }); baseFn_A(); }
2.
Continuation Local Storage (CLS):
CLS is useful for handling context in asynchronous environments like Node.js, where data needs to persist across asynchronous calls without explicitly passing it through each function.
javascript
const { AsyncLocalStorage } = require('async_hooks'); const asyncLocalStorage = new AsyncLocalStorage(); function setRequireFn(data) { const store = asyncLocalStorage.getStore() || {}; asyncLocalStorage.enterWith({...store, ...data}); } function requireFn() { return asyncLocalStorage.getStore() || {}; }
While this works in Node.js, it is not available in browsers as the async_hooks​ module does not exist there.
3.
Compromise: Passing Context Variables:
As a workaround, you can pass a ctx​ object explicitly and modify it as needed.
javascript
function createContext() { return { op: () => 'default op logic', op2: () => 'default op2 logic', op3: () => 'default op3 logic', op4: () => 'default op4 logic', }; } function baseFn_A(ctx) { const { op, op2, op3, op4 } = ctx; return [op(), op2(), op3(), op4()]; } function higherFn_B(ctx) { const newCtx = { ...ctx, op: () => 'New Op logic' }; return baseFn_A(newCtx); } function higherFn_C(ctx) { const newCtx = { ...ctx, op: () => 'op result', op2: () => 'op2 result', op3: () => 'op3 result', op4: () => 'op4 result', }; return higherFn_B(newCtx); }

Conclusion:

This approach allows context to be modified dynamically, offering a cleaner way to achieve Dependency Injection or similar effects in JavaScript. For now, however, there are still limitations in certain environments (like browsers), and more work is needed to generalize these techniques.