之前写了 对于依赖注入的思考 一文,得到了许多网友的指点,发现自己的理解存在很大的偏差。
经过一段时间的学习,我发现我要解决的问题和依赖注入这个名词的关系不大,这块要解决的问题其实更像是 Algebraic Effects(代数效应),和依赖注入相同的地方在于,它们都是为了解决编程工程化上的一些难题(我简单的理解为在不修改旧代码的情况下可以替换其中的函数实现)
对于依赖注入的思考-代码示例
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派生),许多需要的方法都是要从这个上下文中来获取。这个是实现一个类似代数效应以及依赖注入的方法,也等价于实现了一个可以操作的闭包环境,可以像在编码时替换一个闭包变量的实际指向一样容易的在运行时替换。最终就是为了部分的解决表达式问题(Expression Problem)
需要解决的问题
在不修改旧代码的情况下修改他的逻辑
寻找解决方案
最显而易见的:参数传递
只要在编写旧代码的时候将需要被替换的函数作为参数就行了:
function baseFn_A(op){ return op(); } function higherFn_B(op){ baseFn_A(op); } 通过传递一个函数,就可以在不修改 higherFn_B 函数代码的前提下(不修改旧代码)来改变旧代码的逻辑了。这也是最简单的依赖倒置的实现。但它有一个重大的问题,那就是在很多层嵌套以及我需要传递很多参数的情况下,代码会非常的繁琐。
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 声明需要哪些参数,但中间的传递层可以选择覆盖其中的一些配置,也可以选择不用传递的话就好了,伪代码如下:
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 的可以尝试运行以下代码:
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 异步上下文提案讨论] 这个提案似乎还遥遥无期。
妥协的办法:传递上下文变量
这个算是 "最显而易见的:参数传递" 办法的一种变种: 这里理论上还可以通过利用 react 所实现的代数效应来实现不传递 ctx 参数(在 vue 中就是利用 inject )
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' ] 