美文网首页
依赖注入1:引言

依赖注入1:引言

作者: 闲杂人等 | 来源:发表于2020-05-07 22:10 被阅读0次

    在看了一通SpringBoot、Spring frmework框架后始终觉得一头雾水,搞不透彻,无奈从最基本的概念开始学起.找到一本很多年前的书Dependency Injection看了第一章觉得内容非常有助于我理解IoC和DI,整理出来便于记忆和复习。这一篇中对ID要解决的问题开始,遍历了以往的解决方案,并层层递进的指出每一个方案的解决的问题和带来的问题,又由上一个解决方案的缺点引出下一个解决方案。将DI的来龙去脉清晰的展现在读者面前,有助于深入理解DI。

    1. 准备

    1.1 术语

    1. service: 对外提供可调用方法的对象
    2. client: 服务的使用者;
    3. Dependency:依赖项,提供服务的对象
    4. Dependent:依赖者,调用服务者,既依赖于依赖项者

    1.2 类比

    • Object Grpha:对象图,client -----> server 形成了对象图,既把依赖关系视为图

    2. 问题

    如何简化Object Grpha的构造?

    3. 在DI之前是如何解决的

    3.1实例

    图1

    上图展示了一个最简单的两个对象的依赖关系。用代码可如此表示:

    public class Emailer {
        private SpellChecker spellChecker;
        public Emailer() {
           //这个new是问题所在
            this.spellChecker = new SpellChecker();
        }
        public void send(String text) {
            if (! spellChecker.check(text)) {
                throw new IllegalArgumentException("test spell-checking failed!");
            }
           //continue;
        }
    }
    

    当我们想要对这Emailer 类编写单元测试时,发现很难测试:

    public EmailerTest {
      @Test
        void testEmailerSend() {
            SpellChecker mockSpellChecker = Mockito.mock(SpellChecker.class);
            given(mockSpellChecker.check(anyString())).willReturn(true);
            Emailer emailer = new Emailer();
            emailer.send("test");
            verify(mockSpellChecker, times(1)).check(anyString());
        }
    }
    

    这段测试代码是无法使用的,因为我们设置了SpellChecker后,无法为Emailer设置,因为Emailer中SpellChecker是在构造函数中new出来的,所以无法用Mock SpellChecker的方式对Emailer的SpellChecker进行设置。
    除了无法进行测试外,这段代码还有存在着问题:
    如果SpellChecker需要支持多种文字的拼写检查时,我们我们需要针对每一类语言的SpellChecker编写不同Emailer,如:当我们需要支持英语时不但需要一个EnglishSpellChecker,还需要一个EnglishEmailer,需要FrenchSpellChecker时,就需要一个FrenchSpellChecker。而这一切罪魁祸首就是this.spellChecker = new SpellChecker();。这行代码将Emailer对SpellChecker这个依赖项的创建封装在了自己的构造函数里。

    3.2 第一次改进 :Construction by hand

    改进后的代码

    public class Emailer {
        private SpellChecker spellChecker;
        public Emailer(SpellChecker checker) {
           //由原来的new改为传入一个对象
            //this.spellChecker = new SpellChecker();
            this.spellChecker = checker;
        }
        public void send(String text) {
            if (! spellChecker.check(text)) {
                throw new IllegalArgumentException("test spell-checking failed!");
            }
           //continue;
        }
    }
    

    还要将SpellChecker提取为abstract类,使得Emailer依赖于抽象类SpellChecker

    看看是否可测试:
    运行前述的测试代码,代码通过测试

    在看看如何应对针对每一中语言的SpellChecker,需要对应一个Emailer的问题:需要使用Emailer的客户端在使用时Emaile只需传入一个SpellCheker即可,无需再对每一个语言单独建立Emailer了.如:需要英语时:
    Emailer emailerService = new Emailer(new EnglishSpellChecker)需要法语时:Emailer emailerService = new Emailer(new FrenchSpellChecker)
    这种在构建一个对象时将其所需依赖项的实例传入的方式被称为“构造器注入”。这种注入方式被称为Construction by hand 也被称为:manual injection。中文含义为:手工注入。有此称呼是因为使用Emailer的程序员承担了初始化依赖项(SpellChecker)并将其传入Emailer构造器中的责任。
    让我们来看看这种方式的问题:

    1. 使用Emailer的人需要了解Emailer的装配细节,需要了解Emailer依赖于一个类型为SpellChecker的对象
    2. 所谓需要了解Emailer的细节,意味着使用时需要依赖了解到的这些装配细节来装备它,也就意味着一旦Emailer的实现有了改变,你可能就需要同样的改变装配部分的代码。
      打个比方:你购买了一个机械装置,厂家邮来的不是一个开箱即用,组装好的产品,你需要自己按照说明书组装,这种装配很复杂和耗时,需要你花些气力学会它。更麻烦的是其中有几个组件是需要你按照它的要求自行采购的。当你熟悉了装配,也熟悉了如何采购不提供的组件后,你又订购了一台,可恨的事情发生了,你发现装配方式变了,需要额外采购的配件规格也变了,于是你不得不重新学习,不得不再去找适当的厂商供应新配件。如果你是个这个设备的大宗用户,你带领了一群人使用它,你既负责培训他们使用方式,又要采购配件,然后每次新增或新换的设备都不同......。

    那我们如何才能把了解依赖关系,并负责装备的这个责任推出去呢?有一个答案就是工厂模式。以上面的例子举例,就是你要选一个直接发给你成品用的工厂。回到代码上就是,我们要把装备的责任推给除depency和denpendent两方之外的第三方:Factory。

    3.3 第二次改进:Factory Pattern

    object graph 变化为:


    图2

    代码见:
    Emailer的代码暂不做改动
    将创建和组装Emailer的责任放到EmailFactory中:

    public class EmailerFactory {
        private static Emailer emailerInstance;
    
        public static void set(Emailer emailer) {
            emailerInstance= emailer;
        }
    
        public Emailer newEnglishEmailer() {
            if (emailerInstance == null) {
                Emailer service = new Emailer();
                service.setSpellChecker(new EnglishSpellChecker());
                emailerInstance = service;
                return service;
            }
          //...
         // 其他语言的
          //...
            return emailerInstance;
    
        }
    }
    

    使用Emailer的调用方调用方式: Emailer emailer = new EmailerFactory().newEnglishEmailer()。无需知道EnglishSpellChecker的存在
    看看测试代码如何:

     @Test
        void testEmailerInFactoryPattern() {
          //准备测试的前置条件
            SpellChecker mockSpellChecker = Mockito.mock(SpellChecker.class);
            given(mockSpellChecker.check(anyString())).willReturn(true);
            //调用被测试部分的代码
            Emailer emailer = new Emailer();
            emailer.setSpellChecker(mockSpellChecker);
            emailer.send("test");
            //验证
            verify(mockSpellChecker, times(1)).check(anyString());
        }
    

    到目前为止似乎不错,但需求变更来了,要求为Emailer添加地址本,于是代码:

    @Data
    public class EmailerFactory {
        private static Emailer emailerInstance;
    
        public static void set(Emailer emailer) {
            emailerInstance= emailer;
        }
    
        public Emailer newEnglishEmailer() {
            if (emailerInstance == null) {
                Emailer service = new Emailer();
                service.setSpellChecker(new EnglishSpellChecker());
                service.setAddressBook(new EmailBook());
                service.setTextEditor(new SimpleEnglishTextEditor());
                emailerInstance = service;
                return service;
            }
            return emailerInstance;
    
        }
        public Emailer newJapaneseEmailer() {
            if (emailerInstance == null) {
                Emailer service = new Emailer();
                service.setSpellChecker(new JapaneseChecker());
                service.setAddressBook(new EmailBook());
                service.setTextEditor(new SimpleJapaneseTextEditor());
                emailerInstance = service;
                return service;
            }
            return emailerInstance;
        }
        public Emailer newFrenchEmailer() {
            if (emailerInstance == null) {
                Emailer service = new Emailer();
                service.setSpellChecker(new FreanchChecker());
                service.setAddressBook(new EmailBook());
                service.setTextEditor(new SimpleFreanchTextEditor());
                emailerInstance = service;
                return service;
            }
            return emailerInstance;
        }
    }
    
    

    然后又要更换新的地址簿,于是:

    ....
    public Emailer newJapaneseEmailer() {
    ......
        service.setAddressBook(new PhoneAndEmailBook());
    }
    public Emailer newFrenchEmailer() {
    ......
        service.setAddressBook(new PhoneAndEmailBook());
    }
    
    public Emailer newEnglishEmailer() {
    ......
        service.setAddressBook(new PhoneAndEmailBook());
    }
    
    
    

    这个变化都是相同的!这显然不是我们想要的结果。进一步讲,客户端代码对工厂的可用性是无能为力的。你必须创建新的工厂为每一个新增的入object grap的变量。所有这些代码都要编写、测试和维护
    在这个Emailer的例子中,会不可避免的出现下列用法:

    
    Emailer service = new EmailerFactoryFactory()
                                 .newAdvancedEmailerFactory()
                                 .newJapaneseEmailerWithPhoneAndEmail();
    

    以上代码体现了作为集中装配的工厂为了对每一个变量产生对应的工厂,产生了嵌套的工厂。这对于测试时极难的。由此可见,Factory Pattern技术的缺陷时很严重的,尤其在大规模的、较为复杂的代码中。我们对此核心难题(随着object graph中节点的增加,工厂的数量也随之增加)更广泛的缓解

    3.4 第三次改进: The Service Locator Pattern

    The Service Locator Pattern是一种工厂。是一个第三方object负责生成完整的object graph。
    用法会是这样的形式:Emailer emailer = (Emailer)new ServiceLocator().get("Emailer");注意到我们需要给Service Locator传递一个服务的key值。(感觉有点像Spring Cloud中的服务发现和注册组件如NACOS)
    这种模式作为一种工厂,同样具有工厂具有的缺点,并且其key-service的绑定如果错误,客户端就会得到错误的服务对象。

    3.5 拥抱 Dependency Injection

    3.5.1 好莱坞原则

    “Don‘t call us; we'll call you!”

    public class Emailer {
        ......
       public void send(String text) { ... }
    }
    
    public class SimpleEmailClient {
        private Emailer emailer;
        public SimpleEmailClient(Emailer emailer) {
            this.emailer = emailer;
        }
        public void sendEmail() {
            emailer.send(readMessage());
       }
    }
    

    在上面的例子中SimpleEmailClient是dependent(client),Emailer是dependency(service)。注意两个类都没有意识到如何构建自己的object graph,也没有调用第三者的服务来生成某个对象。
    为了发送email,SimpleEmailClient不需要暴露任何关于Emailer和Emailer如何工作的信息。换句话说,SimpleEmailClient封装了Emailer,发送email对用用户来说是不透明的。构建和连接依赖项现在是由依赖注入器完成的。如下图


    图3

    要注意,SimpleEmailClient对于它所需要的Emailer到底属于哪一种类型(如:EnglishEmailer还是JapaneseEmailer)一无所知。它所知的就是它需要某一类型的Emailer。还要注意到client端的代码在不知道service端的创建逻辑或如何定位它的情况下,现在可以组装依赖。DI能够促使杂乱的代码形成流水线式或洋葱式(层层剥离),使我们更关注目的而非底层逻辑。
    到目前我们还没看到这种组装时如何完成的,装配方式会因注入器的不同而有所不同;但是上述内容还是很有意义的因为它显示了基础设施(那些负责装配和构建的代码)的代码和应用代码(service要完成的核心功能)的分离。

    相关文章

      网友评论

          本文标题:依赖注入1:引言

          本文链接:https://www.haomeiwen.com/subject/yleaghtx.html