谈谈自己对依赖注入的理解

news2024/9/23 3:17:07

1. 絮絮叨叨

1.1 想学习Google Guice

  • 在工作的过程中,发现有名的大数据组件Presto大量使用Google Guice实现各种Module的构建

  • 很多bind(interface).to(implementClass).in(scope)语句,实现接口与实现类的绑定,并指定实现类是单例还是多例

    // 接口:ExchangeClientSupplier,实现类:ExchangeClientFactory,scope:单例
    binder.bind(ExchangeClientSupplier.class).to(ExchangeClientFactory.class).in(Scopes.SINGLETON);
    
  • 在使用时,通过@Inject注解实现依赖注入时,就会自动将ExchangeClientFactory的实例传入

    @Inject
    public LocalQueryProvider(
            QueryManager queryManager,
            ExchangeClientSupplier exchangeClientSupplier,
            ... // 省略
            )
    
  • 在分析Presto运行时,某个类属于哪个Module,或者具体使用哪个实现类,需要有一定的Google Guice的知识

  • 因此,笔者有了学习Google Guice的打算

1.2 依赖注入,引起关注

  • 在学习Google Guice时,发现很多资料都说这是一个依赖注入(Dependency Injection, DI)框架
  • 结合以前毕业找工作时,Spring中常见问题就有依赖注入或IoC(Inversion of Control,控制反转)
  • 回想一下,自己只知道依赖注入怎么用,它怎么来的、有什么好处等一无所知
  • 小学数学老师经常说的,半灌水,叮当响,就是我了 😂
  • 通过阅读一些资料,自己对依赖注入有了一定的理解,在此想记录并分享自己对依赖注入的理解

1.3 dependent class vs dependency class

  • 我们通过邮箱进行邮件的收发,实际我们使用的是EmailClient,而EmailClient依靠EmailService实现收发邮件。
  • 一旦EmailService宕机,EmailClient将无法使用。在这样的场景下,我们称EmailClient依赖于EmailService
    • 将EmailClient称作dependent classdependant class,即依附于其他类的类
    • 将EmailService称作dependency class,即被依赖的类
  • 为什么是dependent class和dependent class ?
    • 对英语单词的理解,有时总会打破自己的认知,需要结合上下文才能正确理解
    • 自己才疏学浅,自己印象中dependent含义为:依赖的、依靠的,是个形容词,而dependency含义为:依赖,是个名词
    • 要是让自己为上述两个类取名,depending class和depended class( be + 动词ing形式,表示主动表示主动;be + 动词过去式,表示被动 😏 )
    • 看了外国人对这两种类的命名后发现,dependent的名词释义,是非常贴合该场景的。
    • 而dependency,笔者还没找到感觉,可能是因为约定俗成。例如,在maven中被依赖的第三方类,是以<dependency></dependency>的形式引入
  • Stack Overflow上,一个不错的讨论:What does dependency/dependant mean?

2. 使用new主动创建依赖

2.1 最原始的编程习惯

  • 还是以邮箱场景为例,代码简单编写如下

  • 邮箱底层使用的EmailService可能发生变化,比如,谷歌的、QQ的、网易等邮件服务都有可能被选为EmailService

  • 先定义EmailService接口,然后实现一个谷歌的EmailService

    public interface EmailService {
        // 发送邮件
        void sendEmail(String targetAccount, String content);
    
        // 查看新邮件
        void getEmail();
    
        // 是否有新邮件
        boolean hasNewEmail();
    }
    
    public class GoogleEmailService implements EmailService{
    
       @Override
       public void sendEmail(String targetAccount, String content) {
           System.out.printf("GoogleEmailService: send an email to %s, content: \n%s\n", targetAccount, content);
       }
    
       @Override
       public void getEmail() {
           System.out.printf("GoogleEmailService: get an email from the remote ...");
       }
    
       @Override
       public boolean hasNewEmail() {
           return true;
       }
    }
    
  • 然后创建EmailClient,内部使用GoogleEmailService作为EmailService

    public class EmailClient {
        private EmailService service;
    
        public EmailClient() {
            this.service = new GoogleEmailService();
        }
    
        public void sendEmail(String targetAccount, String content) {
            service.sendEmail(targetAccount, content);
        }
    
        public void receiveEmail() throws InterruptedException {
            while (true) {
                if (service.hasNewEmail()) {
                    service.getEmail();
                }
                Thread.sleep(60 * 1000); // 每隔60s,检查一次是否有新邮件到达
            }
        }
    }
    

2.2 存在的问题

问题1:通过new创建依赖,dependent class的代码难以维护

  • 这里的GoogleEmailService构造函数没有入参,因此通过new进行创建十分简单
  • 以后随着需求的变更,GoogleEmailService的构造函数会发生变化,例如需要传入url、port、account等信息
  • 这时,EmailClient的代码也需要随之变化

问题2:难以进行单元测试

  • 像EmailService这种外部服务,在进行单元测试时,如果连接真实的EmailService,对测试环境的稳定性、纯洁性有极高要求
  • 因此,使用mock模拟EmailService的方式更为简单、快速和可靠,具体可以参考之前的博客:Java单元测试
  • EmailClient通过new创建EmailService,使用mock方式是难以进行单元测试的
  • 要想使用mock出来的EmailService,要么创建一个新的构造函数,要么通过setter进行重新设置
  • setter方法进行设置,其实为时已晚,因为已经创建好了一个真实的EmailService 😭

根本原因

  • 出现上述问题的原因是:dependent class与dependency class紧耦合
  • 紧耦合导致dependency class任何改动都会影响到dependent class,且难以对dependent class进行单元测试

3. 使用工厂模式

3.1 工厂模式改造代码

  • 既然直接new一个dependency class,会使得dependent class难以维护,那我使用工厂模式不就行了

  • dependency class的任何变化,最终都只会影响工厂类,dependent class直接享用现成的对象

  • 使用工厂模式(改良版的工厂方法模式),提供EmailService对象

    public class GoogleEmailServiceFactory {
        public static EmailService getEmailService() {
            return new GoogleEmailService();
        }
    }
    
    public class QQEmailServiceFactory {
        public static EmailService getEmailService() {
            return new QQEmailService(); // QQEmailService类代码省略,可以参考GoogleEmailService
        }
    }
    
  • EmailClient更新为如下代码:

    public class EmailClient {
        private EmailService service;
    
        public EmailClient() {
            this.service = GoogleEmailServiceFactory.getEmailService();
        }
    
        ... // 其他代码省略
    }
    
  • 通过工厂模式,成功实现了dependent class与dependency class的解耦,能解决上面的问题1

3.2 还是存在一些问题

  • 使用工厂类,只是实现了耦合的转移:将dependent class与dependency class的紧耦合转移到了工厂类与dependency class

  • 但同时还增加了EmailClient与工厂类之间的耦合,而且单元测试也需要额外的代码才能实现

  • 例如,可以通过setter方法,向Factory注入mock出来的EmailService,以便进行单元测试

    public class GoogleEmailServiceFactory {
        private static EmailService service;
    
        public static EmailService getEmailService() {
            if (service != null) {
                return service;
            }
            return new GoogleEmailService();
        }
    
        public static void setService(EmailService mockService) {
            service = mockService;
        }
    }
    
  • 单元测试代码如下:

    public class EmailClientTest {
        @Test
        public void sendEmailTest() {
            // mock出EmailService病注入factory
            EmailService mock = mock(EmailService.class);
            GoogleEmailServiceFactory.setService(mock);
            // 创建client,factory返回的是mock出来的EmailService
            EmailClient emailClient = new EmailClient();
            // 调用emailClient.sendEmail()方法,将调用mock的sendEmail()方法
            emailClient.sendEmail("sunrise@gmail.com", "Hello lucy!");
            verify(mock).sendEmail("sunrise@gmail.com", "Hello lucy!");
        }
    }
    

4. 依赖注入

4.1 IoC(控制反转)

  • 通过new创建依赖,或者使用工厂模式提供依赖,都存在一些问题
  • 在笔者看来,这些问题的根因在于,依赖的使用者需要自己负责依赖的创建与管理(何时创建,何时销毁;单例,还是多例等)
  • 如果能将一个已经创建好的依赖直接给到dependent class,则dependent class的代码维护、单元测试都将变得简单
  • 因此,我们希望有一些容器或框架能自动创建并管理依赖,然后将依赖以某种方式给到dependent class
  • 就像去面馆吃面,你需要自己去端煮好的面、取餐具、加调料,哪天店里布局变了,你可能都找不到免费泡菜在哪里
  • 如果面馆老板能将面、餐具、配菜自己送上来,自己就坐在那里玩手机,是不是变得体验很好了?
  • 在这样的情况下,dependent class对依赖的控制,就交给了外部的容器或框架,控制权发生了转换
  • Inversion of Control,控制反转,由此而得名
  • 这与著名的好莱坞法则,Don’tcall us; we’ll call you!,不谋而合

4.2 依赖注入

  • IoC是一种设计思想,为了实现这一设计思想,出现了依赖注入这一技术
  • 依赖注入,即由容器或框架将创建好的对象(依赖)自动注入到dependent class中
  • 实现依赖注入的途径主要有两种:(1)通过构造函数实现依赖注入,Constructor Dependency Injection,CDI;(2)通过setter方法实现依赖注入,Setter Dependency Injection,SDI
  • 注意:
    • 这里的实现,并不是说构造函数或setter方法将依赖注入到dependent class,而是由构造函数或setter方法只提供依赖注入的入口
    • 依赖注入这个动作需要由外部去完成,如容器或框架

4.2.1 通过构造函数实现依赖注入

  • 通过构造函数实现注入,描述起来废话一大堆,其实看看示例代码就一下子明白了

  • EmailClient修改如下,后续便可以通过构造函数实现EmailService的注入

    public class EmailClient {
        private EmailService service;
    
        public EmailClient(EmailService service) {
            this.service = service;
        }
    	... // 其他代码省略
    }
    

4.2.2 通过setter方法实现依赖注入

  • 同样地,看代码示例就能明白:

    public class EmailClient {
        private EmailService service;
        // setter方法,设置初始化service
        public void setService(EmailService service) {
            this.service = service;
        }
        ... // 其他代码省略
    }
    
  • 不管是构造函数注入,还是通过setter方法注入,单元测试都将变得简单

  • 以setter方法注入为例,单元测试如下:

    @Test
    public void sendEmailTest() {
        // mock出EmailService并注入factory
        EmailService mockService = mock(EmailService.class);
        // 创建client,通过setter方法注入mock出来的EmailService
        EmailClient emailClient = new EmailClient();
        emailClient.setService(mockService);
        // 调用emailClient.sendEmail()方法,将调用mock的sendEmail()方法
        emailClient.sendEmail("sunrise@gmail.com", "Hello lucy!");
        verify(mockService).sendEmail("sunrise@gmail.com", "Hello lucy!");
    }
    

4.2.3 不常见的方法:通过接口进行依赖注入

  • 博客,依赖注入原理(为什么需要依赖注入) ,还讲到了通过接口实现依赖注入

  • 实现方法:定义一个接口,该接口含有一个能注入依赖的抽象方法;dependent class实现该接口,便可以允许依赖的注入

    // 依赖注入的接口
    public interface EmailServiceInjector {
        void injectEmailService(EmailService service);
    }
    // 实现该接口以允许依赖注入
    public class EmailClient implements EmailServiceInjector {
        private EmailService service;
    
        @Override
        public void injectEmailService(EmailService service) {
            this.service = service;
        }
        ... // 其他代码省略
    }
    

4.3 依赖注入框架

  • 到目前为止,我们只是让dependent class预留了依赖注入的入口,要想实现依赖注入,还需要外部辅助
  • 目前,有很多成熟的依赖注入框架,它们负责依赖的创建、管理以及注入
  • 例如,Spring的依赖注入、Google Guice都是有名的依赖注入框架,又称依赖注入的容器,Dependency Injection Containers ,DIC
  • 后续,笔者将学习Google Guice,这一大名鼎鼎的依赖注入框架

5. 后记

5.1 参考链接

  • Spring – Difference Between Dependency Injection and Factory Pattern
  • good basic introduction:Inversion of Control and Dependency Injection in Java
  • 基于Spring讲解Ioc与DI:Spring – Difference Between Inversion of Control and Dependency Injection
  • Spring中的各种依赖注入方式:Intro to Inversion of Control and Dependency Injection with Spring
  • 用小说的形式讲解Spring(2) —— 注入方式哪家强
  • 大神之作,有空拜读:Inversion of Control Containers and the Dependency Injection pattern
  • 视频,介绍Google Guice的motivation、usage:Google I/O 2009 - Big Modular Java with Guice

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/98771.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Service的绑定过程

前言 Service的绑定过程将分为两个部分来进行讲解&#xff1b;分别是Contextlmpl到AMS的调用过程和Service的绑定过程。 frameworks/base/core/java/android/content/ContextWrapper.javapublic boolean bindService(Intent service, ServiceConnection conn,int flags) {ret…

计算机毕设Python+Vue-新型冠状病毒防控咨询网站(程序+LW+部署)

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

根据端口划分虚拟局域、集线器、中继器、交换机、路由器、网桥----计算机网络

集线器&#xff1a; 连接计算机和交换机&#xff0c;类似于多台中继器。 实现多台电脑的同时使用一个进线接口来上网或组成局域网 中继器&#xff1a; 连接两条电缆&#xff0c;作用是放大前一条电缆里面的信号并传入下一条电缆。 是对接收到的信息进行再生放大&#xff0c;以…

Jenkins + Jmeter + Ant 持续集成

搭建提前安装好&#xff1a;ant Jenkins 环境 一、Jenkins 安装 Ant 插件&#xff1a; 进入Jenkins 配置插件页面&#xff0c;安装ant 插件&#xff1a; 打开插件配置页面&#xff0c;如下图&#xff1a; 点击“Available” 在输入框搜索 ant 安装即可&#xff1a; 二、安装…

计算机毕业设计springboot+vue基本微信小程序的透析耗材管理系统

项目介绍 随着信息技术和网络技术的飞速发展,人类已进入全新信息化时代,传统管理技术已无法高效,便捷地管理信息。为了迎合时代需求,优化管理效率,各种各样的管理程序应运而生,各行各业相继进入信息管理时代,透析耗材管理小程序就是信息时代变革中的产物之一。 任何程序都要遵循…

vue前端案例教学:动态获取最新疫情数据展示(代码详解)

【辰兮要努力】&#xff1a;hello你好我是辰兮&#xff0c;很高兴你能来阅读&#xff0c;昵称是希望自己能不断精进&#xff0c;向着优秀程序员前行&#xff01; 博客来源于项目以及编程中遇到的问题总结&#xff0c;偶尔会有读书分享&#xff0c;我会陆续更新Java前端、后台、…

[leetcode.4]寻找两个正序数组的中位数 多思考边界

题目展示 题目要点 题目其实本身的思路非常简单&#xff0c;就是把两个数组给合并起来&#xff0c;然后寻找中位数&#xff0c;具体可以参考我们使用归并排序时候的最后一步&#xff0c;这题的难点其实在于&#xff08;1&#xff09;时间压缩到lognm&#xff08;2&#xff09;…

[附源码]Nodejs计算机毕业设计基于与协同过滤算法的竞赛项目管理Express(程序+LW)

该项目含有源码、文档、程序、数据库、配套开发软件、软件安装教程。欢迎交流 项目运行 环境配置&#xff1a; Node.js Vscode Mysql5.7 HBuilderXNavicat11VueExpress。 项目技术&#xff1a; Express框架 Node.js Vue 等等组成&#xff0c;B/S模式 Vscode管理前后端分…

基于MMEngine和MMDet搭建目标跟踪框架MMTracking-1.0.0rc1-Win10

缘由&#xff1a; 1. 目标跟踪是工业检测和学术研究的基础课题&#xff0c;其pipeline通常分为视频目标检测、视频实例分割、单目标跟踪、多目标跟踪和Re-ID五类&#xff0c;同时&#xff0c;还细分为在线检测和离线检测两种任务模式。由于现阶段关于目标跟踪的教程较少&#…

机器学习 KNN算法原理

目录 一&#xff1a;KNN算法概念 二&#xff1a;KNN原理 三&#xff1a;KNN超参数 四&#xff1a;KNN算法选择 一&#xff1a;KNN算法概念 KNN(K-Nearest Neighbor)法即K最邻近法&#xff0c;最初由Cover和Hart于1968年提出&#xff0c;是最简单的机器学习算法之一 算法思路…

NNDL 作业9:分别使用numpy和pytorch实现BPTT

6-1P&#xff1a;推导RNN反向传播算法BPTT. 6-2P&#xff1a;设计简单RNN模型&#xff0c;分别用Numpy、Pytorch实现反向传播算子&#xff0c;并代入数值测试.、 forward&#xff1a; 我们知道循环卷积网络的cell的计算公式为&#xff1a; stf(UxtWst−1)\mathrm{s}_tf(…

UML学习入门

UML 举一个简单的例子&#xff0c;来看这样一副图&#xff0c;其中就包括了UML类图中的基本图示法。 首先&#xff0c;看动物矩形框&#xff0c;它代表一个类&#xff08;Class&#xff09;。类图分三层&#xff0c;第一层显示类的名称&#xff0c;如果是抽象类&#xff0c;则…

多模块之前的消息引用问题

多模块引用之前的问题目录概述需求&#xff1a;设计思路实现思路分析1.在第一个模块中调用另一个调用模块的Bean2.普通java类 如何获取另一个模块的Bean3.启用消息机制4.为什么普通java类很难调用SpringBean的&#xff1f;参考资料和推荐阅读Survive by day and develop by nig…

机器学习快速上手基础

努力是为了不平庸~ 学习的最大理由是想摆脱平庸&#xff0c;早一天就多一份人生的精彩&#xff1b;迟一天就多一天平庸的困扰。 目录 一、简介与应该明确的学习方法 1、学习方法 2、简介 二、学习机器学习的优势 三、机器学习的内核 1、学习术语 2、内核 四、机器学习…

IT团队自动化端点管理解决方案—基于虹科Chef工具

企业如何简化端点管理工作流程&#xff1f; 如何解决安全事件&#xff1f;如何生成审计报告&#xff1f;如何处理大量账单&#xff1f;这些都是企业IT团队需要考虑的问题。然而&#xff0c;IT人员每天有大量的事情需要处理&#xff0c;如何自动化、标准化、及时性的处理这些问…

(最优化理论与方法)第六章无约束优化算法-第二节:梯度类算法

文章目录一&#xff1a;次梯度算法&#xff08;1&#xff09;次梯度算法结构&#xff08;2&#xff09;应用举例-LASSO问题求解二&#xff1a;牛顿法&#xff08;1&#xff09;经典牛顿法&#xff08;2&#xff09;修正牛顿法三&#xff1a;拟牛顿法&#xff08;1&#xff09;拟…

知到/智慧树——英语听说:实境主题与技能(参考答案)

目录 第一章测试 第二章测试 第三章测试 第四章测试 第五章测试 第六章测试 第七章测试 第八章测试 第九章测试 第十章测试 第一章测试 第1部分总题数: 10 1 【多选题】 (10分) What does this chapter mainly teach &#xff08; &#xff09;&#xff1f; A. T…

生成树(基础)

目录 一、生成树的相关概念 二、最小生成树的相关概念 最小生成树的性质(MST性质)&#xff1a; MST性质解释&#xff1a; 三、Prim算法&#xff08;普里姆算法&#xff09; 动态演示 关键算法&#xff1a; 完整代码&#xff1a; 四、Kruskal(克鲁斯卡尔)算法 动态演示&…

mysql主从复制架构

MySQL的主从复制架构的分布机制&#xff0c;是通过将MySQL的某一台主机(master)的数据复制到其他主机(slave)上。 在复制过程中一台主机作为主服务器&#xff0c;其他服务器则为从服务器。主服务器将更新写入到日志文件中&#xff0c;日志被从服务器的 I/O线程读取&#xff0c;…

逻辑回归 预测癌症数据

目录 一&#xff1a;加载数据 二&#xff1a;数据集划分 三&#xff1a;选择算法 四&#xff1a;网格模型 超参调优 五&#xff1a;模型预测 六&#xff1a;模型保存和使用 七&#xff1a;完整源码分享 八&#xff1a;预测与实际比对 一&#xff1a;加载数据 from sk…