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 class
或dependant class
,即依附于其他类的类 - 将EmailService称作
dependency class
,即被依赖的类
- 将EmailClient称作
- 为什么是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!"); } }
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是一种设计思想,为了实现这一设计思想,出现了依赖注入这一技术
- 依赖注入,即由容器或框架将创建好的对象(依赖)自动注入到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 参考链接
- Spring – Difference Between Dependency Injection and Factory Pattern
- good basic introduction:Inversion of Control and Dependency Injection in Java
- 基于Spring讲解Ioc与DI:Spring – Difference Between Inversion of Control and Dependency Injection
- Spring中的各种依赖注入方式:Intro to Inversion of Control and Dependency Injection with Spring
- 用小说的形式讲解Spring(2) —— 注入方式哪家强
- 大神之作,有空拜读:Inversion of Control Containers and the Dependency Injection pattern
- 视频,介绍Google Guice的motivation、usage:Google I/O 2009 - Big Modular Java with Guice