Java 浅谈依赖注入的实现

知無涯 for 新潮测试技术 · 2022年06月18日 · 3751 次阅读

作者:软件质量保障
知乎: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 不做赘述,后面会单独出一篇文章详细介绍)。

暂无回复。
需要 登录 后方可回复, 如果你还没有账号请点击这里 注册