浅谈依赖注入的实现
持续坚持原创输出,点击蓝字关注我吧

作者:软件质量保障
知乎:https://www.zhihu.com/people/iloverain1024
Hello,小伙伴们好久不见。这段时间项目并发,手上有多个项目在跟进,还有专项在做,可谓是鸭梨山大。
针对Java中的依赖注入、控制反转概念,想必测试同学都不陌生(面试八股文走起....),恰好这段时间做的专项有使用到这些技术,“实践出真知”,经过动手操作获得知识要比啃概念理解的更深刻记忆的更牢固。下面就聊聊我对依赖注入的理解。当然,作为“非专业开发”,文中如有纰漏之处,还请各位同行赐教,给我留言指出,我好及时订正,以免造成误导。
概述
In software engineering, dependency injection is a technique whereby one object (or static method) supplies the dependencies of another object. A dependency is an object that can be used (a service).
这是维基百科的定义,但它并不是特别容易理解。在开始介绍依赖注入之前,让我们了解下编程中的依赖是什么意思。当 A 类使用 B 类的某些功能时,则表示 A 类具有 B 类的依赖关系。
在Java中,在使用其他类的方法之前,我们首先需要创建该类的对象(即A类需要创建B类的实例)。因此,将创建对象的任务转移给容器(例如spring容器),并直接使用依赖项称为依赖注入,下面这张图就描绘的比较生动形象。

依赖注入的实现
依赖注入能够消除程序开发中的硬编码式的对象间依赖关系,使应用程序松散耦合、可扩展和可维护,将依赖性问题的解决从编译时转移到运行时。
假设要实现发送电子邮件的功能,如果不考虑依赖注入,我们可以像下面这样实现。
EmailService类包含将电子邮件消息发送到收件人电子邮件地址的逻辑。代码如下所示:
package cn.qa.dependencyInjection.service;public class EmailService {public void sendEmail(String message, String receiver){//logic to send emailSystem.out.println("Email sent to "+receiver+ " with Message="+message);}}
package cn.qa.dependencyInjection.application;import cn.qa.dependencyInjection.service.EmailService;public class MyApplication {private EmailService email = new EmailService();public void processMessages(String msg, String rec){//do some msg validation, manipulation logic etcthis.email.sendEmail(msg, rec);}}
测试代码如下,将MyApplicationTest类作为发送电子邮件客户端逻辑。
package cn.qa.dependencyInjection.application;class MyApplicationTest {public static void main(String[] args) {MyApplication app = new MyApplication();app.processMessages("Hi Pankaj", "pankaj@abc.com");}}
乍一看,上面的实现似乎没有什么问题,事实上这样写的代码逻辑有一定的局限性。
-
MyApplication类负责初始化电子邮件服务,然后使用邮件服务发送邮件,但这会导致硬编码依赖。如果将来我们想切换到其他高级电子邮件服务,则需要更改 MyApplication类中依赖服务,这使得我们的应用程序难以扩展,如果电子邮件服务用于多个类,那改起来就更难了。
-
如果我们想扩展我们的应用程序以提供额外的通讯功能,例如 SMS 或 Facebook消息,那么我们需要为此编写另一个应用程序,同样这也将涉及应用程序类和客户端类中的代码更改。
-
测试应用程序将非常困难,因为我们的应用程序直接创建电子邮件服务实例,我们无法在测试类中Mock这些对象。
现在让我们看看如何应用依赖注入模式来解决上述问题。Java实现依赖注入需要注意以下几点:
-
服务组件应设计有基类或接口。
-
消费者类应该按照服务接口来实现。
-
注入器类实现初始化服务和消费者类。
三者关系如下:

服务组件
定义MessageService为服务实现的接口类。
package cn.qa.dependencyInjection.service;public interface MessageService {void sendMessage(String msg, String rec);}
下面来实现MessageService接口的电子邮件EmailServiceImpl和短信服务SMSServiceImpl代码如下:
package cn.qa.dependencyInjection.serviceImpl;import cn.qa.dependencyInjection.service.MessageService;public class EmailServiceImpl implements MessageService {@Overridepublic void sendMessage(String msg, String rec){System.out.println("Email sent to "+rec+ " with Message="+msg);}}package cn.qa.dependencyInjection.serviceImpl;import cn.qa.dependencyInjection.service.MessageService;public class SMSServiceImpl implements MessageService {@Overridepublic void sendMessage(String msg, String rec) {//logic to send SMSSystem.out.println("SMS sent to "+rec+ " with Message="+msg);}}
我们需要的依赖注入的服务已经开发完毕,现在我们可以开发消费者类了。
服务消费者
Consumer为消费者类接口:
package cn.qa.dependencyInjection.consumer;public interface Consumer {void processMessages(String msg, String rec);}
消费者类实现代码如下所示。
package cn.qa.dependencyInjection.application;import cn.qa.dependencyInjection.consumer.Consumer;import cn.qa.dependencyInjection.service.MessageService;public class MyDIApplication implements Consumer {private MessageService service;public MyDIApplication(MessageService svc){this.service=svc;}@Overridepublic void processMessages(String msg, String rec){//do some msg validation, manipulation logic etcthis.service.sendMessage(msg, rec);}}
可以看到我们的应用程序类只是在调用服务接口类,使用服务接口调用可以使我们通过Mock MessageService的方式轻松测试应用程序,当然这个过程发生在服务运行时而不是编译时。
现在我们准备开发依赖注入器类。
依赖注入器类
定义一个MessageServiceInjector接口类。
package cn.qa.dependencyInjection.injector;import cn.qa.dependencyInjection.consumer.Consumer;public interface MessageServiceInjector {public Consumer getConsumer();}
现在,为每个服务SMSService/EmailService创建如下注入器类:
package cn.qa.dependencyInjection.injector;import cn.qa.dependencyInjection.application.MyDIApplication;import cn.qa.dependencyInjection.consumer.Consumer;import cn.qa.dependencyInjection.serviceImpl.EmailServiceImpl;public class EmailServiceInjector implements MessageServiceInjector{@Overridepublic Consumer getConsumer() {return new MyDIApplication(new EmailServiceImpl());}}
package cn.qa.dependencyInjection.injector;import cn.qa.dependencyInjection.application.MyDIApplication;import cn.qa.dependencyInjection.consumer.Consumer;import cn.qa.dependencyInjection.serviceImpl.SMSServiceImpl;public class SMSServiceInjector implements MessageServiceInjector{@Overridepublic Consumer getConsumer() {return new MyDIApplication(new SMSServiceImpl());}}
现在看看我们的客户端应用程序将如何通过一段简单的代码调用SMSService/EmailService服务。
package cn.qa.dependencyInjection.application;import cn.qa.dependencyInjection.consumer.Consumer;import cn.qa.dependencyInjection.injector.EmailServiceInjector;import cn.qa.dependencyInjection.injector.MessageServiceInjector;import cn.qa.dependencyInjection.injector.SMSServiceInjector;public class MyMessageDITest {public static void main(String[] args) {String msg = "Hi QA";String email = "QA@abc.com";String phone = "4088888888";MessageServiceInjector injector = null;Consumer app = null;//Send emailinjector = new EmailServiceInjector();app = injector.getConsumer();app.processMessages(msg, email);//Send SMSinjector = new SMSServiceInjector();app = injector.getConsumer();app.processMessages(msg, phone);}}
代码中可以看到,服务类是在注入器中创建的。此外,如果我们进一步扩展我们的应用程序以实现Facebook 消息发送,我们将只需要编写服务类和注入器类。
因此依赖注入解决了硬编码依赖的问题,并使我们的应用程序灵活且易于扩展。
下面让我们看看通过Mock注入器和服务类来测试应用程序类是多么容易。
测试用例
package cn.qa.dependencyInjection.application;import cn.qa.dependencyInjection.consumer.Consumer;import cn.qa.dependencyInjection.injector.MessageServiceInjector;import cn.qa.dependencyInjection.service.MessageService;import org.junit.After;import org.junit.Before;import org.junit.Test;public class MyDIApplicationJUnitTest {private MessageServiceInjector injector;@Beforepublic void setUp(){// mock the injector with anonymous classinjector = new MessageServiceInjector() {@Overridepublic Consumer getConsumer() {//mock the message servicereturn new MyDIApplication(new MessageService() {@Overridepublic void sendMessage(String msg, String rec) {System.out.println("Mock Message Service implementation");}});}};}@Testpublic void test() {Consumer consumer = injector.getConsumer();consumer.processMessages("Hi Pankaj", "pankaj@abc.com");}@Afterpublic void tear(){injector = null;}}
使用 DI 的优缺点
优点:
-
有助于单元测试。
-
依赖项的初始化是由依赖注入器完成的,因此样板代码减少了。
-
扩展应用程序变得更容易。
-
有助于松散耦合,这点在应用程序编程中很重要。
缺点:
-
学习起来有点复杂,如果过度使用会导致依赖管理不当问题。
-
许多编译时错误被推送到运行时才能发现。
能够高效实现DI的框架
-
Spring
-
Google Guice (本文不对guice不做赘述,后面会单独出一篇文章详细介绍)。
- END -
下方扫码关注 软件质量保障,与质量君一起学习成长、共同进步,做一个职场最贵Tester!
-
后台回复【测开】获取测试开发xmind脑图
-
后台回复【加群】获取加入测试社群!
浙公网安备 33010602011771号