• 浅谈依赖注入的实现


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

    作者:软件质量保障
    知乎: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设计模式

    细读《阿里测试之道》

    我在阿里做测开

  • 相关阅读:
    Which is best in Python: urllib2, PycURL or mechanize?
    Ruby开源项目介绍(1):octopress——像黑客一样写博客
    Truncated incorrect DOUBLE value解决办法
    Qt Quarterly
    Rich Client Platform教程
    iOS6 中如何获得通讯录访问权限
    省赛热身赛之Javabeans
    [置顶] [开心学php100天]第三天:不羁的PHP文件操作
    hdu2033 人见人爱A+B
    [置顶] AAM算法简介
  • 原文地址:https://www.cnblogs.com/iloverain/p/16515060.html
Copyright © 2020-2023  润新知