谈谈自己对依赖注入的理解

1. 絮絮叨叨

1.1 想学习Google Guice

  • 在工作的过程中,发现有名的大数据组件Presto大量使用Google Guice实现各种Module的构建

  • 很多bind(interface).to(implementClass).in(scope)语句,实现接口与实现类的绑定,并指定实现类是单例还是多例

    // 接口:ExchangeClientSupplier,实现类:ExchangeClientFactory,scope:单例
    binder.bind(ExchangeClientSupplier.class).to(ExchangeClientFactory.class).in(Scopes.SINGLETON);
    
  • 在使用时,通过@Inject注解实现依赖注入时,就会自动将ExchangeClientFactory的实例传入

    @Inject
    public LocalQueryProvider(
            QueryManager queryManager,
            ExchangeClientSupplier exchangeClientSupplier,
            ... // 省略
            )
    
  • 在分析Presto运行时,某个类属于哪个Module,或者具体使用哪个实现类,需要有一定的Google Guice的知识

  • 因此,笔者有了学习Google Guice的打算

1.2 依赖注入,引起关注

  • 在学习Google Guice时,发现很多资料都说这是一个依赖注入(Dependency Injection, DI)框架
  • 结合以前毕业找工作时,Spring中常见问题就有依赖注入或IoC(Inversion of Control,控制反转)
  • 回想一下,自己只知道依赖注入怎么用,它怎么来的、有什么好处等一无所知
  • 小学数学老师经常说的,半灌水,叮当响,就是我了 😂
  • 通过阅读一些资料,自己对依赖注入有了一定的理解,在此想记录并分享自己对依赖注入的理解

1.3 dependent class vs dependency class

  • 我们通过邮箱进行邮件的收发,实际我们使用的是EmailClient,而EmailClient依靠EmailService实现收发邮件。
  • 一旦EmailService宕机,EmailClient将无法使用。在这样的场景下,我们称EmailClient依赖于EmailService
    • 将EmailClient称作dependent classdependant class,即依附于其他类的类
    • 将EmailService称作dependency class,即被依赖的类
  • 为什么是dependent class和dependent class ?
    • 对英语单词的理解,有时总会打破自己的认知,需要结合上下文才能正确理解
    • 自己才疏学浅,自己印象中dependent含义为:依赖的、依靠的,是个形容词,而dependency含义为:依赖,是个名词
    • 要是让自己为上述两个类取名,depending class和depended class( be + 动词ing形式,表示主动表示主动;be + 动词过去式,表示被动 😏 )
    • 看了外国人对这两种类的命名后发现,dependent的名词释义,是非常贴合该场景的。
    • 而dependency,笔者还没找到感觉,可能是因为约定俗成。例如,在maven中被依赖的第三方类,是以<dependency></dependency>的形式引入
  • Stack Overflow上,一个不错的讨论:What does dependency/dependant mean?

2. 使用new主动创建依赖

2.1 最原始的编程习惯

  • 还是以邮箱场景为例,代码简单编写如下

  • 邮箱底层使用的EmailService可能发生变化,比如,谷歌的、QQ的、网易等邮件服务都有可能被选为EmailService

  • 先定义EmailService接口,然后实现一个谷歌的EmailService

    public interface EmailService {
        // 发送邮件
        void sendEmail(String targetAccount, String content);
    
        // 查看新邮件
        void getEmail();
    
        // 是否有新邮件
        boolean hasNewEmail();
    }
    
    public class GoogleEmailService implements EmailService{
    
       @Override
       public void sendEmail(String targetAccount, String content) {
           System.out.printf("GoogleEmailService: send an email to %s, content: \n%s\n", targetAccount, content);
       }
    
       @Override
       public void getEmail() {
           System.out.printf("GoogleEmailService: get an email from the remote ...");
       }
    
       @Override
       public boolean hasNewEmail() {
           return true;
       }
    }
    
  • 然后创建EmailClient,内部使用GoogleEmailService作为EmailService

    public class EmailClient {
        private EmailService service;
    
        public EmailClient() {
            this.service = new GoogleEmailService();
        }
    
        public void sendEmail(String targetAccount, String content) {
            service.sendEmail(targetAccount, content);
        }
    
        public void receiveEmail() throws InterruptedException {
            while (true) {
                if (service.hasNewEmail()) {
                    service.getEmail();
                }
                Thread.sleep(60 * 1000); // 每隔60s,检查一次是否有新邮件到达
            }
        }
    }
    

2.2 存在的问题

问题1:通过new创建依赖,dependent class的代码难以维护

  • 这里的GoogleEmailService构造函数没有入参,因此通过new进行创建十分简单
  • 以后随着需求的变更,GoogleEmailService的构造函数会发生变化,例如需要传入url、port、account等信息
  • 这时,EmailClient的代码也需要随之变化

问题2:难以进行单元测试

  • 像EmailService这种外部服务,在进行单元测试时,如果连接真实的EmailService,对测试环境的稳定性、纯洁性有极高要求
  • 因此,使用mock模拟EmailService的方式更为简单、快速和可靠,具体可以参考之前的博客:Java单元测试
  • EmailClient通过new创建EmailService,使用mock方式是难以进行单元测试的
  • 要想使用mock出来的EmailService,要么创建一个新的构造函数,要么通过setter进行重新设置
  • setter方法进行设置,其实为时已晚,因为已经创建好了一个真实的EmailService 😭

根本原因

  • 出现上述问题的原因是:dependent class与dependency class紧耦合
  • 紧耦合导致dependency class任何改动都会影响到dependent class,且难以对dependent class进行单元测试

3. 使用工厂模式

3.1 工厂模式改造代码

  • 既然直接new一个dependency class,会使得dependent class难以维护,那我使用工厂模式不就行了

  • dependency class的任何变化,最终都只会影响工厂类,dependent class直接享用现成的对象

  • 使用工厂模式(改良版的工厂方法模式),提供EmailService对象

    public class GoogleEmailServiceFactory {
        public static EmailService getEmailService() {
            return new GoogleEmailService();
        }
    }
    
    public class QQEmailServiceFactory {
        public static EmailService getEmailService() {
            return new QQEmailService(); // QQEmailService类代码省略,可以参考GoogleEmailService
        }
    }
    
  • EmailClient更新为如下代码:

    public class EmailClient {
        private EmailService service;
    
        public EmailClient() {
            this.service = GoogleEmailServiceFactory.getEmailService();
        }
    
        ... // 其他代码省略
    }
    
  • 通过工厂模式,成功实现了dependent class与dependency class的解耦,能解决上面的问题1

3.2 还是存在一些问题

  • 使用工厂类,只是实现了耦合的转移:将dependent class与dependency class的紧耦合转移到了工厂类与dependency class

  • 但同时还增加了EmailClient与工厂类之间的耦合,而且单元测试也需要额外的代码才能实现

  • 例如,可以通过setter方法,向Factory注入mock出来的EmailService,以便进行单元测试

    public class GoogleEmailServiceFactory {
        private static EmailService service;
    
        public static EmailService getEmailService() {
            if (service != null) {
                return service;
            }
            return new GoogleEmailService();
        }
    
        public static void setService(EmailService mockService) {
            service = mockService;
        }
    }
    
  • 单元测试代码如下:

    public class EmailClientTest {
        @Test
        public void sendEmailTest() {
            // mock出EmailService病注入factory
            EmailService mock = mock(EmailService.class);
            GoogleEmailServiceFactory.setService(mock);
            // 创建client,factory返回的是mock出来的EmailService
            EmailClient emailClient = new EmailClient();
            // 调用emailClient.sendEmail()方法,将调用mock的sendEmail()方法
            emailClient.sendEmail("sunrise@gmail.com", "Hello lucy!");
            verify(mock).sendEmail("sunrise@gmail.com", "Hello lucy!");
            // 最重要的代码:测试完后,需要清除测试中传入的值,因为在Factory中service是一个全局变量
            GoogleEmailServiceFactory.setService(null);
        }
    }
    
  • 关注单元测试的最后一行代码,由于service是一个static variable,如果不进行重置操作,很可能引发意想不到的问题。例如,影响其他使用到GoogleEmailServiceFactory的单元测试,或无法进行并发测试

    GoogleEmailServiceFactory.setService(null);
    

4. 依赖注入

4.1 IoC(控制反转)

  • 通过new创建依赖,或者使用工厂模式提供依赖,都存在一些问题
  • 在笔者看来,这些问题的根因在于,依赖的使用者需要自己负责依赖的创建与管理(何时创建,何时销毁;单例,还是多例等)
  • 如果能将一个已经创建好的依赖直接给到dependent class,则dependent class的代码维护、单元测试都将变得简单
  • 因此,我们希望有一些容器或框架能自动创建并管理依赖,然后将依赖以某种方式给到dependent class
  • 在这样的情况下,dependent class对依赖的控制权,就交给了外部的容器或框架,控制权发生了转换
  • Inversion of Control,控制反转,由此而得名
  • 这与著名的好莱坞法则,Don’tcall us; we’ll call you!,不谋而合

4.2 依赖注入

  • IoC是一种设计思想,为了实现这一设计思想,出现了依赖注入这一技术或设计模式(Design Pattern
  • 依赖注入,即由容器或框架将创建好的对象(依赖)自动注入到dependent class中
  • 实现依赖注入的途径主要有两种:(1)通过构造函数实现依赖注入,Constructor Dependency Injection,CDI;(2)通过setter方法实现依赖注入,Setter Dependency Injection,SDI
  • 注意:
    • 这里的实现,并不是说构造函数或setter方法将依赖注入到dependent class,而是由构造函数或setter方法只提供依赖注入的入口
    • 依赖注入这个动作需要由外部去完成,如容器或框架

4.2.1 通过构造函数实现依赖注入

  • 通过构造函数实现注入,描述起来废话一大堆,其实看看示例代码就一下子明白了

  • EmailClient修改如下,后续便可以通过构造函数实现EmailService的注入

    public class EmailClient {
        private EmailService service;
    
        public EmailClient(EmailService service) {
            this.service = service;
        }
    	... // 其他代码省略
    }
    

4.2.2 通过setter方法实现依赖注入

  • 同样地,看代码示例就能明白:

    public class EmailClient {
        private EmailService service;
        // setter方法,设置初始化service
        public void setService(EmailService service) {
            this.service = service;
        }
        ... // 其他代码省略
    }
    
  • 不管是构造函数注入,还是通过setter方法注入,单元测试都将变得简单

  • 以setter方法注入为例,单元测试如下:

    @Test
    public void sendEmailTest() {
        // mock出EmailService并注入factory
        EmailService mockService = mock(EmailService.class);
        // 创建client,通过setter方法注入mock出来的EmailService
        EmailClient emailClient = new EmailClient();
        emailClient.setService(mockService);
        // 调用emailClient.sendEmail()方法,将调用mock的sendEmail()方法
        emailClient.sendEmail("sunrise@gmail.com", "Hello lucy!");
        verify(mockService).sendEmail("sunrise@gmail.com", "Hello lucy!");
    }
    

4.2.3 不常见的方法:通过接口进行依赖注入

  • 博客,依赖注入原理(为什么需要依赖注入) ,还讲到了通过接口实现依赖注入

  • 实现方法:定义一个接口,该接口含有一个能注入依赖的抽象方法;dependent class实现该接口,便可以允许依赖的注入

    // 依赖注入的接口
    public interface EmailServiceInjector {
        void injectEmailService(EmailService service);
    }
    // 实现该接口以允许依赖注入
    public class EmailClient implements EmailServiceInjector {
        private EmailService service;
    
        @Override
        public void injectEmailService(EmailService service) {
            this.service = service;
        }
        ... // 其他代码省略
    }
    

4.3 依赖注入框架

  • 到目前为止,我们只是让dependent class预留了依赖注入的入口,要想实现依赖的自动注入,还需要依赖注入框架的辅助
  • 这些依赖注入框架,可以负责依赖的创建、管理以及注入
  • 例如,Spring的依赖注入、Google Guice都是有名的依赖注入框架,又称依赖注入的容器,Dependency Injection Containers ,DIC
  • 后续,笔者将学习Google Guice,这一大名鼎鼎的依赖注入框架

5. 后记

5.1 总结

  • 如果使用一句话来描述依赖注入,我想引用Google Guice官方的一句话

    Dependency injection is a design pattern wherein classes declare their dependencies as arguments instead of creating those dependencies directly.

5.2 参考链接

  • 同类型的、最好的article


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值