本文转载自公众号“读芯术”(ID:AI_Discovery)。
为什么依赖注入对于程序员来说是件好事?本文将用Kotlin编写示例说明这一问题的,但其中并未使用该特殊语法功能,因此每位程序员都能理解。
如果咖啡机不在咖啡馆应该怎么办?如果有一个空白空间,每次想喝咖啡时,就必须从头开始构造机器。软件中的运行速度要比在现实中快一百万倍,因此可以合理编写如下代码:
- classProgrammer {
- funtakeBreak() {
- //constructing anew coffee machine:
- val coffeeMachine =EspressoCoffeeMachine()
- coffeeMachine.pourCoffee()
- .into(coffeeCup)
- .awaitCompletion()
- drink(coffeeCup)
- }
- }
但问题是,使用空的构造函数实际上并不能构造咖啡机。虽然结构性能也许不算一个因素,但使用咖啡机时所涉及的结构复杂性一定包含在内:
- classProgrammer {
- funtakeBreak() {
- val waterTank =waterTankProvider.getWaterTank()
- if (waterTank ==null) {
- //TODO: handlefailed to provide water tank.
- return
- }
- val beansContainer =beansContainerProvider.getBeansContainer()
- if (beansContainer ==null) {
- //TODO: handlefailed to provide beans container.
- return
- }
- val milkContainer =milkContainerProvider.getMilkContainer()
- if (milkContainer ==null) {
- //TODO: handlefailed to provide milk container
- return
- }
- val milkPump =milkPumpProvider.getMilkPump()
- if (milkPump ==null) {
- //TODO: handlefailed to provide milk pump
- return
- }
- //constructing anew coffee machine:
- val coffeeMachine =EspressoCoffeeMachine(
- waterTank,
- beansContainer,
- milkContainer,
- milkPump,
- )
- coffeeMachine.pourCoffee()
- .into(coffeeCup)
- .awaitCompletion()
- drink(coffeeCup)
- }
- }
你能从代码中看出程序员在休息时做了什么吗?
仅仅因为无法构建咖啡机,程序员就不喝咖啡而return(返回)工作吗?在喝咖啡前,程序员怎么可能与所有providers(供应商)交谈呢?这些都是可怜的programmer(程序员)的担忧,他们只想喝到咖啡。
可以把它们都转移到另一个用于构造咖啡机的类中,实际上,这就是工厂设计模式(Factorydesign pattern)。
- classProgrammer {
- funtakeBreak() {
- //Constructs new CoffeeMachineFactory
- val coffeeMachineFactory =CoffeeMachineFactory()
- val coffeeMachine =coffeeMachineFactory.create()
- coffeeMachine.pourCoffee()
- .into(coffeeCup)
- .awaitCompletion()
- drink(coffeeCup)
- }
- }
它清洁了代码,但这迟早会不得不再次改变。程序员们被宠坏了:我们喜欢尝试来自不同工厂、不同机器所制造的咖啡。
- classProgrammer {
- funtakeBreak() {
- val brand =brandProvider.getBrand()
- if (brand ==null) {
- //TODO: handlefailed to provide brand
- return
- }
- valcoffeeMachineFactory =coffeeMachineFactoryProducer.produceFactoryBy(brand)
- if (coffeeMachineFactory ==null) {
- //TODO: handlefailed to provide CoffeeMachineFactory
- return;
- }
- val machineModel =coffeeMachineModelProvider.getMachineModel()
- if (machineModel ==null) {
- //TODO: handle failed to provideCoffeeMachineModel
- return;
- }
- val coffeeMachine = coffeeMachineFactory.create(machineModel)
- coffeeMachine.pourCoffee()
- .into(coffeeCup)
- .awaitCompletion()
- drink(coffeeCup)
- }
- }
如你所见,工厂延迟了构造复杂性,但并未使其消失。
图源:unsplash
在现实生活中,建造工具和使用工具是两个完全分离的过程。人类一直在利用这一点,史前人类打磨好矛枪后才准备刺杀猛犸象。
程序员不需要在休息时忙着构造咖啡机。他们当然不需要从工厂带来一个新机器,咖啡机只是程序员用来快速制作咖啡的工具罢了,这样就能回到自己真正的工作中:写代码!
那么,依赖注入与这些有什么关系呢?
依赖注入是一种将类的构造代码与其使用过程进行系统化分离的体系结构方法。方法有几种,构造依赖倒置(constructiondependencies inversion)就是其中之一。
它意味着CoffeeMachine(咖啡机)的构造与使用咖啡机的Programmer(程序员)不应该紧密耦合。相反,Programmer(程序员)的构造直观上应该依赖于CoffeeMachine(咖啡机)。
- classProgrammer/*constructor*/(
- //class member automatically assigned bythe constructor
- privateval coffeeMachine:CoffeeMachine
- ) {
- funtakeBreak() {
- coffeeMachine.pourCoffee()
- .into(coffeeCup)
- .awaitCompletion()
- drink(coffeeCup)
- }
- }
但是,这不是仅仅把咖啡机的构造转移到容纳程序员的类中吗?
不一定。举个例子,一家SoftwareCompanyX(软件公司X)希望招到一名Programmer(程序员):再次按照依赖倒置原则,使SoftwareCompanyX的构造依赖于Programmer即可,而不是将Programmer的构造与SoftwareCompanyX紧密耦合。
- classSoftwareCompanyX/*constructor*/(
- //class member automatically assigned bythe constructor
- privateval programmer:Programmer
- ) : /*implements*/SoftwareCompany {
- overridefunstartWorkingDay() {
- programmer.takeBreak()
- }
- }
如此一来,Programmer(程序员)可以轻易转移到另一家SoftwareCompany(软件公司),休闲地喝一杯咖啡。她所需要的只是能为她提供 CoffeeMachine(咖啡机)参考的人,然后她就可以回来工作了。
最终,必须有人来进行构造。这个人将是唯一需要处理特定类群的构造细节的人,这也是他的唯一任务。构成的根源就是大多数依赖注入框架中的Module(模块)。
- classSoftwareCompanyModule {
- funprovideSoftwareCompany():SoftwareCompany {
- returnSoftwareCompanyX(provideProgrammer())
- }
- privatefunprovideProgrammer():Programmer {
- returnAndroidDeveloper(provideCoffeeMachine(provideFactory()))
- }
- privatefunprovideCoffeeMachine(factory:CoffeeMachineFactory):CoffeeMachine {
- returnfactory.create(provideMachineModel())
- }
- privatefunprovideMachineModel():String {
- returnBuildConfig.COFFEE_MACHINE_MODEL
- }
- privatefunprovideFactory():CoffeeMachineFactory {
- returnCoffeeMachineFactory(provideCoffeeMachineBrand())
- }
- privatefunprovideCoffeeMachineBrand():String {
- returnBuildConfig.COFFEE_MACHINE_BRAND
- }
- }
所以,SoftwareCompanyModule (软件公司模块)负责连接一切,并只对外公开SoftwareCompany(软件公司)。
- classSiliconValley {
- privateval softwareCompany:SoftwareCompany
- init {
- softwareCompany =softwareCompanyModule.provideSoftwareCompany()
- }
- funonDayStart() {
- softwareCompany.startWorkingDay()
- }
- }
所以,为什么需要依赖注入框架呢?
以下问题需要得到解答:
- 谁应该将模块实例化?
- 如果一个模块依赖于其他模块该怎么办?
- 怎么在不同地方共享同一对象实例?
- 单元测试怎么样?
图源:unsplash
依赖注入框架有助于应对这些挑战,这样就能专注于自己所在领域的挑战,而不用从头开始设计。咖啡时间结束了,希望你能学到一些新知识。