首页

对于依赖注入的思考-二

english:Reflections on Dependency Injection
之前写了 对于依赖注入的思考 一文,得到了许多网友的指点,发现自己的理解存在很大的偏差。
经过一段时间的学习,我发现我要解决的问题和依赖注入这个名词的关系不大,
这块要解决的问题其实更像是 Algebraic Effects(代数效应,和依赖注入相同的地方在于,他们都是为了解决编程工程化上的一些难题(我简单的理解为在不修改旧代码的情况下可以替换其中的函数实现)
js
function baseFn_A(){} function baseFn_B(){} function higherFn_B(){ baseFn_A() //other baseFn ... } function higherFn_C(){ higherFn_B() //other baseFn / higherFn ... } // 这中间还可能有更多层次的这样的套娃 // 上面存在一个依赖链路 higherFn_C>higherFn_B>baseFn_A // 如果我要实现一个新的函数 higherFn_C2 ,它和 higherFn_C 唯一的区别就是调用的是 baseFn_B 而非 baseFn_A // 我认为依赖注入就是为了更方便的创建 higherFn_C2 而不需要改动特别多的代码
其实在2021年我就触碰过这个问题( js 一个函数执行的时候如何判断自己是否处于另一个的调用栈中?,这个提问现在看来显然是一个 XY problem
下面是我刚刚得到的一些灵感,记录在手机记事本中
上下文传递方法使用一个同名变量ctx,起始于一个全局变量ctx然后从这个全局变量派生出来一棵树,每个模块乃至于函数都会有自己的ctx(从上级ctx 派生),许多需要的方法都是要从这个上下文中来获取
这个是实现一个类似代数效应以及依赖注入的方法
也等价于实现了一个可以操作的闭包环境,可以像在编码时替换一个闭包变量的实际指向一样容易的在运行时替换
最终就是为了部分的解决表达式问题 https://en.m.wikipedia.org/wiki/Expression_problem
现在我捋一捋这个灵感

需要解决的问题

在不修改旧代码的情况下修改他的逻辑

寻找解决方案

最显而易见的:参数传递

只要在编写旧代码的时候将需要被替换的函数作为参数就行了
js
function baseFn_A(op){ return op(), } function higherFn_B(op){ baseFn_A(op) }
通过传递一个函数,就可以在不修改 higherFn_B 函数代码的前提下(不修改旧代码)来改变旧代码的逻辑了
这也是最简单的依赖倒置的实践
上面这个实现是可行的,但在实际使用中会遇到下面这样的情况,很多层嵌套,很多的参数需要传递
js
function baseFn_A(op,op2,op3,op4....){ return [op(),op2(),op3(),op4()....()] } function higherFn_B(op,op2,op3,op4....){ baseFn_A(op,op2,op3,op4....) }
如果可以做到 baseFn_A 声明需要哪些参数,但中间的传递层可以选择覆盖其中的一些配置,也可以选择不用传递的话就好了,伪代码如下
js
function baseFn_A(){ const {op,op2,op3,op4....} = requireFn() return [op(),op2(),op3(),op4()....()] } function higherFn_B(){ // 覆盖 higherFn_C 传递的 op,但不影响op2,op3,op4.... setRequireFn({op:()=>{}}) baseFn_A() } function higherFn_C(){ setRequireFn({op,op2,op3,op4....}) higherFn_B() }

CLS来帮忙

CLS(Continuation Local Storage) 是一种在程序执行过程中,用于存储和传递跨异步操作的上下文数据的机制。在异步编程中,尤其是在像 Node.js 这样的环境中,跨越多个异步操作传递数据变得尤为复杂。CLS 允许你在不同的函数和异步任务中共享一些全局的上下文信息,而无需显式地将它们作为参数传递。CLS 的核心思想是:通过将数据与当前的执行上下文关联,确保在程序的控制流跨越异步边界时,这些数据能够随着控制流一起“传递”,而不需要通过显式的函数调用来传递。
在 Node.js 中,AsyncLocalStorage​ 提供了一个实现 CLS 的工具,允许在异步操作的生命周期内存储和访问上下文数据。
在 Node.js 中,AsyncLocalStorage​ 提供了一个实现 CLS 的工具,它允许你在异步操作的生命周期内存储和访问上下文数据。AsyncLocalStorage​ 是基于 async_hooks​ 模块构建的,专门设计用于在异步操作之间共享数据。
安装了 node 的可以尝试运行以下代码
js
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() || {}; } // ----------------------- function baseFn_A() { const { op, op2, op3, op4 } = requireFn(); return [op(), op2(), op3(), op4()]; } function higherFn_B() { setRequireFn({ op: () => 'New Op logic' }); return baseFn_A(); } function higherFn_C() { setRequireFn({ op: () => 'op result', op2: () => 'op2 result', op3: () => 'op3 result', op4: () => 'op4 result' }); return higherFn_B(); } console.log(higherFn_C()); // 输出:['op result', 'op2 result', 'op3 result', 'op4 result']
看上去十分的美好了,但是...我之所以喜欢js就是因为代码可以运行在浏览器和node.js ,上面的代码只能支持node.js 但浏览器中是没有 async_hooks 模块的,【提案讨论】JavaScript 异步上下文 · JSCIG · Discussion #9 · GitHub 这个提案似乎还遥遥无期。

妥协的办法,传递上下文变量

这个算是 最显而易见的:参数传递 办法的一种变种。
这里理论上还可以通过利用 react 所实现的代数效应来实现不传递 ctx 参数(在vue中就是利用 inject )
js
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()]; } // 高阶函数 B,修改上下文并传递给 baseFn_A function higherFn_B(ctx) { const newCtx = { ...ctx, op: () => 'New Op logic' }; // 创建新上下文 return baseFn_A(newCtx); // 将新的上下文传递给 baseFn_A } // 高阶函数 C,设置完整的上下文数据并传递给 higherFn_B function higherFn_C(ctx) { const newCtx = { ...ctx, op: () => 'op result', op2: () => 'op2 result', op3: () => 'op3 result', op4: () => 'op4 result' }; return higherFn_B(newCtx); // 将新的上下文传递给 higherFn_B } const initialCtx = createContext(); // 创建初始上下文 console.log(higherFn_C(initialCtx)); // 输出:[ 'New Op logic', 'op2 result', 'op3 result', 'op4 result' ]