当前位置: 首页 > Web前端 > HTML

精读《依赖注入简介》

时间:2023-03-28 16:04:15 HTML

精读文章:DependencyInjectioninJS/TS–Part1Overview依赖注入就是把函数内部的实现抽象成参数,方便我们控制。原文遵循“如何解决单次测试无法完成的问题,统一依赖注入入口,如何自动保证依赖顺序正确,如何解决循环依赖,top-”的思路downvsbottom-upprogrammingthinking”,依赖注入从思想出发到扩展特性,都连贯起来。如何解决无法做单项测试的问题?如果一个功能内容的实现是一个随机的功能,如何做测试呢?exportconstrandomNumber=(max:number):number=>{returnMath.floor(Math.random()*(max+1));};因为结果失控了,做单考显然不可能,然后Math.如果把随机函数抽象成参数,问题就迎刃而解了!导出类型RandomGenerator=()=>数字;exportconstrandomNumber=(randomGenerator:RandomGenerator,max:number):number=>{returnMath.floor(randomGenerator()*(max+1));};但是带来了一个新的问题:这破坏了randomNumber函数本身的接口,参数变得复杂,不那么好用了。工厂函数+实例模式为了方便业务代码调用,同时导出工厂函数和方便的业务实例就够了!导出类型RandomGenerator=()=>数字;exportconstrandomNumberImplementation=({randomGenerator}:Deps)=>(max:number):number=>{returnMath.floor(randomGenerator()*(max+1));};exportconstrandomNumber=(max:number)=>randomNumberImplementation(Math.random,max);这个乍一看不错,单元测试代码参考了randomNumberImplementation函数,randomGeneratormock是一个固定返回值的函数;业务代码中引用了randomNumber,由于内置了Math.random的实现,使用起来更加自然。只要每个文件都遵循这种双导出的模式,并且业务实现除了传递参数之外没有额外的逻辑,这段代码既可以解决单测问题,也可以解决业务问题。但是带来了一个新的问题:代码中同时存在工厂函数和实例,即同时构造和使用,所以职责不明确,而且因为每个文件都要提前引用依赖,依赖之间很容易形成循环引用,甚至从具体的函数层面来说,函数之间也不存在循环引用。统一依赖注入的入口可以通过统一入口收集依赖来解决这个问题:randomGenerator=Math.random;constfastRandomNumber=makeFastRandomNumber(randomGenerator);constrandomNumber=process.env.NODE_ENV===“生产”?secureRandomNumber:fastRandomNumber;constrandomNumberList=makeRandomNumberList(randomNumber);;导出类型容器=typeofcontainer;在上面的例子中,一个入口文件同时引用了所有的构造函数文件,所以这些构造函数文件之间不需要相互依赖,解决了循环引用的大问题。然后我们依次实例化这些构造函数,传入它们需要的依赖,然后用容器统一导出来使用。用户无需关心如何构建它们,开箱即用。但是带来了新的问题:统一注入的入口代码随着业务文件的变化而变化。同时,如果构造函数之间存在复杂的依赖链,手动维护顺序会变得越来越复杂。:比如A依赖B,B依赖C,如果要初始化C的构造函数,必须先初始化A,再初始化B,最后初始化C。如何自动确保依赖顺序正确?有没有办法修复依赖注入的模板逻辑,使其在调用时根据依赖自动初始化?答案是肯定的,而且非常漂亮:randomNumber:process.env.NODE_ENV!==“生产”?makeFastRandomNumber:()=>secureRandomNumber,randomNumberList:makeRandomNumberList,randomGenerator:()=>Math.random,};typeDependenciesFactories=typeofdependenciesFactories;exporttypeKeyContainer={[在DependenciesFactories]:ReturnValue;};exportconstcontainer={}asContainer;Object.entries(dependenciesFactories).forEach(([dependencyName,factory])=>{returnObject.defineProperty(container,dependencyName,{get:()=>工厂(容器),});});核心代码在Object.defineProperty(container),所有从container[name]访问的函数在调用时都会被初始化,会经过这样一个处理链:初始化容器为空,不提供任何函数,业务代码时不执行任何工厂调用container.randomNumber,触发get(),此时会执行randomNumber的工厂,将容器传入。如果randomNumber工厂不使用任何依赖,则不会访问容器的子键,randomNumber函数创建成功,流程结束。关键步骤就在这里。如果randomNumber工厂使用了任何依赖,假设依赖是它自己,就会陷入死循环。这是代码逻辑错误,应该报错;如果依赖是其他的,假设调用了container.abc,那么就会在abc所在的地方触发get(),重复步骤2,直到成功执行abc的工厂,这样就成功获取到依赖了。神奇的是,固定的代码逻辑会根据访问链接自动嗅探依赖树,并按照正确的顺序,从没有依赖的模块开始执行工厂,逐层往上直到所有依赖顶级包是建立的。每个子模块的构建环节和主模块都分类型,非常漂亮。如何解决循环依赖这里不是说如何解决函数的循环依赖问题,因为:如果函数a依赖函数b,函数b依赖函数a,这相当于a依赖自己,甚至神无法保存。如果能解决,那简直就跟号称发明了永动机一样夸张,根本不用考虑解决这个场景。依赖注入防止模块被引用,因此函数之间不存在循环依赖。为什么说a靠自己连神仙都救不了?a的实现依赖于a。要知道a的逻辑,首先要了解依赖a的逻辑。依赖a的逻辑是找不到的,因为我们是在实现a,所以递归会死循环。依赖注入还需要解决循环依赖问题吗?需要,比如下面的代码:constaFactory=({a}:Deps)=>()=>{return{value:123,onClick:()=>{console.log(a.value);},};};这是循环依赖最极端的场景,依赖自己。但是从逻辑上讲,不存在死循环。如果a实例化后触发onClick,打印123是合理的,但逻辑上不能有歧义。如果不经过特殊处理,a.value确实无法解析。这个问题的解决方法可以参考spring三级缓存的思路,放在精读部分。自上而下vs自下而上的编程思维原文进行了总结和升华,颇有思考价值:依赖注入的思维习惯是自上而下的编程思维,即先思考包之间的逻辑关系,不需要先实际执行。相比之下,自下而上的编程思想需要先实现最后一个没有任何依赖的模块,然后依次实现其他模块,但这种实现顺序不一定符合业务抽象的顺序,也限制了实现过程。精读当我们讨论对象A和对象B之间的相互引用时,spring框架是如何使用三级缓存来解决这个问题的。不管是用spring还是其他框架实现依赖注入,当代码遇到这样的形式时,都会遇到AB循环引用的场景:classA{@inject(B)b;价值=“一”;你好(){控制台.log(“a:”,this.b.value);}}B类{@inject(A)a;值=“b”;hello(){console.log("b:",this.a.value);}}从代码执行的角度来看,a.hello()和b.hello()应该都可以正常执行,因为A和B虽然有循环引用,但是它们的值不构成循环依赖,只要as如果能提前拿到他们的值,输出应该没有问题。但是依赖注入框架遇到了问题。初始化A依赖B,初始化B依赖A。我们再看看spring三级缓存的实现思路:spring三级缓存的含义是:一级缓存,二级缓存,和三级缓存实例半成品实例工厂类实例:实例化+依赖注入初始化完成的实例。半成品实例:仅完成实例化。工厂类:生成半成品实例的工厂。先说过程吧。当AB循环依赖时,框架将以随机顺序初始化。假设先初始化A:1:查找实例A,但是没有一级、二级、三级缓存,所以初始化A,此时只有一个地址,加入L3缓存。Stack:A.Level1cacheLevel2cacheLevel3cachemoduleA?ModuleB2:查找实例A依赖于实例B,寻找实例B,但是没有一二级三级缓存,所以初始化B,此时只有一个地址,将其加入三级缓存。堆栈:A->B。一级缓存二级缓存三级缓存模块A?模块B?三:查找实例B依赖于实例A,查找实例A,因为三级缓存找到了,所以执行三级缓存生成二级缓存。堆栈:A->B->A。Level1cacheLevel2cacheLevel3cacheModuleA??ModuleB?4:因为已经找到实例A的二级缓存,初始化实例B(栈变为A->B),压入一级cache,并清除三级缓存。Stack:A.Level1cacheLevel2cacheLevel3cacheModuleA??ModuleB?5:因为实例A依赖实例B的一级缓存才能找到,实例A被初始化,压入一级缓存,并清除了三级缓存。栈:空。一级缓存二级缓存三级缓存ModuleA?ModuleB?总结依赖注入的本质是将函数内部实现抽象为参数,带来更好的可测试性和可维护性。可维护性是“只需声明依赖,无需关心如何实例化它”。同时,容器的自动初始化也减轻了精神负担。但是最大的贡献是带来了自顶向下的编程思维。依赖注入因为其神奇的特性,需要解决循环依赖问题。这也是一个面试中经常被问到的点,需要牢记,讨论地址为:Jingdu《依赖注入简介》·Issue#440·dt-fe/weekly想参与讨论的请戳这里,每期都有新话题week,周末或周一发布前端精读——帮你过滤靠谱的内容。关注前端精读微信公众号版权声明:免费转载-非商业-非衍生保留属性(CreativeCommons3.0License)