浅谈依赖注入的实现
持续坚持原创输出,点击蓝字关注我吧
作者:软件质量保障
知乎: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 email
System.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 etc
this.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 {
@Override
public 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 {
@Override
public void sendMessage(String msg, String rec) {
//logic to send SMS
System.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;
}
@Override
public void processMessages(String msg, String rec){
//do some msg validation, manipulation logic etc
this.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{
@Override
public 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{
@Override
public 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 email
injector = new EmailServiceInjector();
app = injector.getConsumer();
app.processMessages(msg, email);
//Send SMS
injector = 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;
@Before
public void setUp(){
// mock the injector with anonymous class
injector = new MessageServiceInjector() {
@Override
public Consumer getConsumer() {
//mock the message service
return new MyDIApplication(new MessageService() {
@Override
public void sendMessage(String msg, String rec) {
System.out.println("Mock Message Service implementation");
}
});
}
};
}
@Test
public void test() {
Consumer consumer = injector.getConsumer();
consumer.processMessages("Hi Pankaj", "pankaj@abc.com");
}
@After
public void tear(){
injector = null;
}
}
使用 DI 的优缺点
优点:
-
有助于单元测试。
-
依赖项的初始化是由依赖注入器完成的,因此样板代码减少了。
-
扩展应用程序变得更容易。
-
有助于松散耦合,这点在应用程序编程中很重要。
缺点:
-
学习起来有点复杂,如果过度使用会导致依赖管理不当问题。
-
许多编译时错误被推送到运行时才能发现。
能够高效实现DI的框架
-
Spring
-
Google Guice (本文不对guice不做赘述,后面会单独出一篇文章详细介绍)。
- END -
下方扫码关注 软件质量保障,与质量君一起学习成长、共同进步,做一个职场最贵Tester!
-
后台回复【测开】获取测试开发xmind脑图
-
后台回复【加群】获取加入测试社群!