对于依赖注入的思考-二

9 小时 38 分钟前
 llej

之前写了 对于依赖注入的思考 一文,得到了许多网友的指点,发现自己的理解存在很大的偏差。

经过一段时间的学习,我发现我要解决的问题和依赖注入这个名词的关系不大,这块要解决的问题其实更像是 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' ]

1034 次点击
所在节点    程序员
18 条回复
kk2syc
9 小时 27 分钟前
不如考虑一下 hook
llej
9 小时 23 分钟前
@kk2syc 具体是指什么,react 和 vue 他们的 hook ?
这样是没法解决异步函数调用的问题的
subframe75361
8 小时 12 分钟前
solid.js 的 createContext ?
llej
8 小时 0 分钟前
@subframe75361 没仔细了解过他的实现,猜测应该和 vue 差不多吧,就是在即将执行组件初始化函数之前将上下文设置为一个全局变量,于是组件函数执行的时候就可以获取到那个上下文,问题就是组件函数内异步调用是不行的,例如 vue 组件 setup 模式 中使用 settimeout 延迟过一会再使用 inject() 就会失败,但如果浏览器也支持 cls 的话,就可以实现异步调用也能获取正确的值
netabare
7 小时 59 分钟前
感觉如果语言本身没有直接提供代数效应支持的话,依赖注入还是很麻烦的一件事情
llej
7 小时 58 分钟前
@subframe75361 异步调用这块我之前我忘记写了,正是因为异步这个问题的存在才导致只能使用妥协的办法:传递上下文变量
nomagick
7 小时 55 分钟前


怎么戏这么多

你说的这个不就 this 吗, OOP 啊, 类继承多态啊
llej
7 小时 53 分钟前
@netabare 嗯,依赖注入其实不是问题,但是想要组合式的使用依赖注入(例如 vue 中定义一个 cont use
Config=()=>inject("config
") 然后随意的在任意地方使用这个 useConfig 都能正确的注入值是很难的,需要 cls 或者代数效应这样的方案
llej
7 小时 50 分钟前
@nomagick 一边玩去吧,你的理解没错,我只是在讨论一些稍有不同的情况
galenjiang
7 小时 38 分钟前
@llej Suspense 不就是可以实现异步吗?只是不是语言层面的 algebraic effects ,能力有限。
前端很容易陷入用 react 等框架的实现来理解编程,可以多看一下其他语言的做法。
或者看一下 js 的 metadata, https://www.typescriptlang.org/docs/handbook/decorators.html#metadata ,很多框架用了这个能力。
llej
7 小时 24 分钟前
@galenjiang 你可以看下我 4 楼和八楼的回复,另外你的说法我是认可的
llej
7 小时 11 分钟前
@galenjiang 另外 metadata 还有装饰器这些东西我总感觉有点画蛇添足
galenjiang
6 小时 43 分钟前
@llej 我不太懂,为什么不能用 setTimeout 中使用 inject ,我好久没写了,印象中好像是可以的。还有为什么 useConfig 很难,异步你可以把它作为一个函数 cont useConfig=()=>inject("getConfig"), const getConfig = useConfig(); const config = getConfig()不就好了嘛, 或者 config 直接是一个 promise 都可以 。
meta data 是很优雅的,https://angular.dev/guide/di/dependency-injection#injecting-consuming-a-dependency ,其实它和 inject 可以是等价的。
llej
6 小时 37 分钟前
@galenjiang 不是说 useConfig 很难,而是说这样组合起来到处随便用很难,他只能在组件创建的时候用。

setTimeou 以及请求等异步调用的回掉中是不能使用 inject 的,因为异步执行的时候他没法判断是在那个组件的调用栈中,进而无法找到对应组件树提供的值
llej
6 小时 34 分钟前
@galenjiang 你说的这个异步解决办法是有效的,但这就导致了你的 useConfig 无法被随意组合了,因为组合后又需要像这样包裹一层来调用,等于有了某种传染性
hedwi
6 小时 23 分钟前
angular 用户路过 依赖注入很强大
galenjiang
6 小时 9 分钟前
@llej 我大概明白了,vue 其实是在生成实例时把,把当前实例存在全局变量中,异步是没法通过 getCurrentInstance 访问这个变量的,所以 inject 是依赖 setup ,但是 setup 并没有显式声明这个 this ,所以这里 inject 破坏了依赖注入,使用了一个外部变量
解决的办法是重写 function inject(key, defaultValue) {
let instance = getCurrentInstance(); // 获取当前组件实例
while (instance) {
if (instance.provides && key in instance.provides) {
return instance.provides[key]; // 找到并返回
}
instance = instance.parent; // 继续向上查找
}
return defaultValue; // 未找到,返回默认值
}把 getCurrentInstance 作为一个参数变量传入,而不是直接用全局变量。
llej
6 小时 3 分钟前
@galenjiang 是的,你描述的比我清晰多了,所以这样最后还是绕回了显式传递参数

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://tanronggui.xyz/t/1108620

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX