浅谈依赖注入的实现

​持续坚持原创输出,点击蓝字关注我吧

作者:软件质量保障
知乎: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实现依赖注入需要注意以下几点:

  1. 服务组件应设计有基类或接口。

  2. 消费者类应该按照服务接口来实现。

  3. 注入器类实现初始化服务和消费者类。

三者关系如下:

服务组件

定义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{@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 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 的优缺点

优点:

  1. 有助于单元测试。

  2. 依赖项的初始化是由依赖注入器完成的,因此样板代码减少了。

  3. 扩展应用程序变得更容易。

  4. 有助于松散耦合,这点在应用程序编程中很重要。

缺点:

  1. 学习起来有点复杂,如果过度使用会导致依赖管理不当问题。

  2. 许多编译时错误被推送到运行时才能发现。

能够高效实现DI的框架

  • Spring

  • Google Guice (本文不对guice不做赘述,后面会单独出一篇文章详细介绍)。

- END -


下方扫码关注 软件质量保障,与质量君一起学习成长、共同进步,做一个职场最贵Tester!

  • 后台回复【测开】获取测试开发xmind脑图

  • 后台回复【加群】获取加入测试社群!

往期推荐

聊聊工作中的自我管理和向上管

经验分享|测试工程师转型测试开发历程

聊聊UI自动化的PageObject设计模式

细读《阿里测试之道》

我在阿里做测开

posted @ 2022-07-24 18:23  QualityAssurance21  阅读(38)  评论(0编辑  收藏  举报