正文
我最近在给一个Go service升级重构framework。我和一个朋友提了下,他点评到,搞这种基础升级,就是悟道啊,类似于《禅与摩托车维修艺术》。
他这个说法挺有道理的,大家平时写业务代码,更多是站在地面想着怎么快速完成目标。只有趁升级的时候,才有空飞在1000公里天上,想想为啥要这么设计的哲学问题。
今天就给大家介绍一个重要的基本设计原则:Dependency Injection。这个设计模式在复杂的业务service非常有用,没有它,每次改一个模块的初始化接口,你都要把用到这个模块的代码都改一遍,非常麻烦。
今天很多主流的开源framework都用到了它,比如:
- Guice: Google 维护的一个基于Java的 lightweight dependency injection framework
- Fx: Uber 维护的一个基于Go的dependency injection framework
- AngularJS: Google 维护的基于JavaScript的前端 framework
- Wire: Google维护的Compile-time Dependency Injection for Go
什么是Dependency Injection?
这个翻译成中文叫做依赖注入,用大白话解释就是即插即用。
举个例子,假设你的service里面有个模块A叫“笔记本”,它有个依赖叫“耳机”,用了这个设计原则,你需要听音乐,只用插”耳机“就可以了。后端service中常见的“耳机”依赖有哪些?比如Logging,输出Metrics等。
下面的代码是用 Dependency Injection 创建模块A的伪代码:
func CreateLaptopService() *LaptopService { panic(wire.Build( wire.Struct(new(Logger), "*"), NewHttpClient, NewHeadphoneService, )) }
不用这个原则,有什么后果?
你需要自己搞一堆耳机的原材料,然后自己组装配置。模块A需要耳机的时候,手动装一遍,模块B需要耳机的时候,再手动装一遍。
下面是不用Dependency Injection,创建模块A的伪代码:
func CreateLaptopService() *LaptopService { logger := &Logger{} headphone := &Headphone{} client := NewHttpClient(logger) return NewLaptopService(logger,client,headphone) }
如果service很简单,还可以忍受。但是在业务很复杂时,项目里有上百个依赖的时候就更痛苦了。每次配置”耳机“,你都需要手动把所有模块的接口配置一遍。
下面是不用Dependency Injection,再创建模块B的伪代码:
func CreateDesktopService() *DesktopService { logger := &Logger{} headphone := &Headphone{} client := NewHttpClient(logger) cdDisk := &CdDisk{} cdDrive := &CdDrive{cdDisk} headphone := &Headphone{} return NewLaptopService(logger, client, headphone, cdDrive) }
优点一:减少依赖关系、方便重复使用
有了Dependency Injection,每次配置时,模块A和模块B都是连接到同一个设置的耳机,你只要组装一次耳机。即使有100个模块都需要用耳机,你也只需要组装一次。
优点二:提高可维护性
而对于更复杂的场景,模块B依赖于一个”CD机“,而”CD机“又需要一个”CD碟片“。如果有100个类似的模块都有”CD机“,而你需要做的只是更改”CD机”里的CD碟片,有了Dependency Injection,你也可以省去在“电子厂”里面翻找所有”CD机“的时间,只需要换一张”CD碟片“。
优点三:简化测试流程
每次升级时,只需要测试”耳机“本身的性能,测试不需要和使用”耳机“的代码有任何关联。
总结
最后,划一下重点,Dependency Injection适用的场景,是复杂的大型系统,有很多个服务相互依赖的情况。它能够避免一些重复劳动带来的小错误,提高生产力。如果是一个人写的小玩具,那杀鸡就不用牛刀啦。