本文转载自微信公众号《董泽润的技术笔记》,作者董泽润。转载本文请联系董泽润技术笔记公众号。前几天某服务ut失败,导致其他人无法搭建。查看源码和utcase,发现槽点挺多的,分享下如何修复,以及写单体测试需要注意的几点,由此引出依赖倒置、依赖注入的概念,和控制反转失败casefunctoSeconds(inint64)int64{ifin>time.Now().Unix(){nanosecondSource:=time.Unix(0,in)ifdateIsSane(nanosecondSource){returnnanosecondSource.Unix()}millisecondSource:=time.Unix(0,in*int64(time.Millisecond))ifdateIsSane(millisecondSource){returnmillisecondSource.Unix()}//defaulttonowratherthansendingsomethingstupidreturntime.Now().Unix()}returnin}funcdateIsSane(intime.Time)bool{return(in.Year()>=(time.Now().Year()-1)&&in.Year()<=(time.Now().Year()+1))}函数toSeconds接收一个时间参数,可能是秒,毫秒等时间,经过判断后返回第二个值后……{desc:"lessthannow",args:1459101327,expect:1459101327,},{desc:"greatthanyear",args:now.UnixNano()/6000*6000+7.55424e+17,expect:now.Unix(),},......以上是测试用例表,最后报错greatthanyear。断言失败。简单看一下实现逻辑可以发现函数想要修正为第二个值,但是如果恰好走gcSTW100ms,就会导致expect和实际结果不一致。如何从根本上解决问题?要么修改函数签名,在外层传入time.Now()functoSeconds(inint64,nowtime.Time)int64{...}或者在当前包中定义time.Now函数为变量,修改写单测时的now变量varnow=time.NowfunctoSeconds(inint64)int64{ifin>now().Unix(){......}上面两种方法比较通用,本质是单测ut不应该依赖于当前的系统环境,比如mysql、redis、time等,应该只依赖于入参,多次执行函数时结果应该是一致的。去年遇到更换CI机器,新机器没有redis/mysql,导致一堆ut失败。这是一种不合格的写法。如果依赖环境的资源,就变成了集成测试。如果再进一步依赖业务的状态机,就变成了回归测试,可以说是递进的关系。只有对代码进行单项测试,才能进一步保证其他测试正常。同时,对于单测也不要过于神话,过于追求100%的覆盖率。依赖注入刚才我们很自然地介绍了设计模式,依赖注入非常重要的概念依赖注入functoSeconds(inint64,nowtime.Time)int64简单的说,toSeconds函数调用Systemtime时间。现在,我们将依赖传递给toSeconds在注入依赖项的参数形式。定义很简单。着眼于DI,在设计模式中抽象出四种角色:service我们依赖的对象就像客户端的角色,服务接口定义了客户端如何使用服务的接口。injector注入者角色用于构建服务并将其传递给客户端。让我们看一下图像对象的例子。英雄需要有武器。NewHero是英雄的构造方法typeHerostruct{namestringweaponWeapon}funcNewHero(namestring)*Hero{return&sHero{name:name,weapon:NewGun(),}}这里问题很多,比如换武器AK?当然。但是NewHero在构造的时候依赖NewGun,我们需要在外层初始化武器,然后传入typeHerostruct{namestringweaponWeapon}funcNewHero(namestring,weaWeapon)*Hero{return&Hero{name:name,weapon:wea,}}funcmain(){wea:=NewGun();myhero=NewHero("killer47",wea)}在这个例子中,Hero就是上面提到的client角色,Weapon就是service角色,谁是injector呢?它是主要功能,它实际上是编码器。这个例子还是有问题。原因是武器不应该是一个具体的实例,而是一个接口,也就是上面说的接口作用typeWeaponinterface{Attack(damageint)},也就是说我们的武器应该设计成一个接口Weapon,并且该方法只有一次攻击attack并且附带损害。但直到现在还不理想。比如我没有武器,我就不能攻击人吗?当然我还有手,所以有时候我们需要用Option来实现默认的依赖:name,}for_,option:=rangeoptions{option(i)}ifh.weapon==nil{h.weapon=NewFist()}returnh}typeOptionfunc(*Hero)funcWithWeapon(wWeapon)Option{returnfunc(i*Hero){i.weapon=w}}funcmain(){wea:=NewGun()myhero=NewHero("killer47",WithWeapon(wea))}以上是生产环境下比较理想的方案。不懂的可以跑代码,尝试了解一下第三方框架。刚刚提到的例子比较简单,注入器可以由coder自己完成。但是在很多情况下,依赖对象可能不止一个,也可能有很多,存在交叉依赖。这时候就需要第三方框架支持Java方编写配置文件,使用注解实现。对于go,你可以使用wire,https://github.com/google/wire//+buildwireinjectpackagemainimport("github.com/google/wire""wire-example2/internal/config""wire-example2/internal/db")funcInitApp()(*App,error){panic(wire.Build(config.Provider,db.Provider,NewApp))//调用wire.Build方法传入所有依赖对象和构建最终的函数objecttoget目标对象}和上面类似,定义wire.go文件,然后写+buildwireinject注释,调用wire后会自动生成注入器代码//go:generategorungithub.com/google/wire/cmd/wire//+build!wireinjectpackagemainimport("wire-example2/internal/config""wire-example2/internal/db")//Injectorsfromwire.go:funcInitApp()(*App,error){configConfig,err:=config.New()iferr!=nil{returnil,err}sqlDB,err:=db.New(configConfig)iferr!=nil{returnnil,err}app:=NewApp(sqlDB)returnapp,nil}我们公司有正在使用的一个项目,有兴趣可以看官方Documentation,对搭建很有帮助在大型项目中依赖倒置DIP原则我们也经常听到一个概念,就是依赖倒置原则,它有两个最重要的原则:高层模块不应该依赖低层模块。两者都应该依赖于抽象(例如.,界面)。抽象不应依赖于细节。细节(具体实现)应该依赖于抽象。高层模块不应该依赖低层模块,需要使用接口进行抽象。Abstractions不应该依赖具体的实现,具体的实现应该依赖Abstraction,结合上面的Hero&Weapon案例,应该就很清楚了。那么我们研究DI和DIP设计模式的目的是什么?要让我们程序的各个模块松耦合,并且底层实现的变化不会影响到顶层模块的代码实现,提高模块的可扩展性,但也要有个度。为每个服务做一个接口抽象一个模块是否可行?当然不是。基于这么多年的工程实践,我有一个指导方针分享给大家:EasyChangeable模块需要抽象,跨rpc调用需要抽象。IOC的思想本质上是依赖注入,是IOC的具体实现。在传统编程中,表达程序目的的代码调用库来处理常见任务,但在控制反转中,是框架调用自定义或特定任务代码。更推荐Java党玩家看一下coolshellexamplesharedundo。比如我们有一个set要实现撤消撤回功能{set.data[x]=true}func(set*IntSet)Delete(xint){delete(set.data,x)}func(set*IntSet)Contains(xint)bool{returnset.data[x]}这个是一个IntSet集合,具有Add、Delete、Contains三个函数,现在需要添加undo函数}}func(set*UndoableIntSet)Add(xint){//Overrideif!set.Contains(x){set.data[x]=trueset.functions=append(set.functions,func(){set.Delete(x)})}else{set.functions=append(set.functions,nil)}}func(set*UndoableIntSet)Delete(xint){//Overrideifset.Contains(x){delete(set.data,x)set.functions=append(set.functions,func(){set.Add(x)})}else{set.functions=append(set.functions,nil)}}func(set*UndoableIntSet)Undo()error{iflen(设置函数ions)==0{returnerrors.New("Nofunctionstoundo")}index:=len(set.functions)-1iffunction:=set.functions[index];function!=nil{function()set.functions[index]=nil//Forgarbagecollection}set.functions=set.functions[:index]returnnil}以上就是具体的实现,有什么问题吗?是的,undo理论上只是控制逻辑,但这里是耦合了业务逻辑的具体实现IntSettypeUndo[]func()func(undo*Undo)Add(functionfunc()){*undo=append(*undo,function)}func(undo*Undo)Undo()error{functions:=*undoiflen(functions)==0{returnerrors.New("Nofunctionstoundo")}index:=len(functions)-1iffunction:=functions[索引];function!=nil{function()functions[index]=nil//Forgarbagecollection}*undo=functions[:index]returnnil}上面就是我们Undo的实现,根本不用关心业务逻辑typeIntSetstruct{datamap[int]boolundoUndo}funcNewIntSet()IntSet{returnIntSet{data:make(map[int]bool)}}func(set*IntSet)Undo()error{returnset.undo.Undo()}func(set*IntSet)Contains(xint)bool{returnset.data[x]}func(set*IntSet)Add(xint){if!set.Contains(x){set.data[x]=trueset.undo.Add(func(){set.Delete(x)})}else{set.undo.Add(nil)}}func(set*IntSet)Delete(xint){ifset.Contains(x){delete(set.data,x)set.undo.Add(func(){set.Add(x)})}else{set.undo.Add(nil)}}这是控制反馈反过来,控制逻辑Undo不再依赖于业务逻辑IntSet,而是业务逻辑IntSet依赖于Undo。如果想看更多细节,可以看coolshell的博客,再举两个例子。我们有lbs服务,它会定期更新驱动程序。坐标流中间需要处理很多业务流程。我们埋下了很多hook点。业务逻辑只需要注册对应的点即可。添加新的业务逻辑不需要更改主流程的代码。很多公司都在做中台,比如阿里。原来每个业务线都有自己的业务处理逻辑,每个业务线都有工程师,只写自己业务相关的代码。平台化将共享流程抽象化,每个新业务只需要配置文件即可。可以自定义需要哪些模块,其实就是一种控制反转的思想