zoukankan      html  css  js  c++  java
  • 深入理解IoC和DI

    本文章转载自: https://segmentfault.com/a/1190000005602011


    最近在研究php的lumen框架和phalcon框架,这两个框架的底层架构都用到了IoC,DI,关于这两个概念自己一直没理解更清晰,找到一篇写得非常好的博文,在此做个备份记录。

    基本概念


    依赖倒置原则(DIP)(Dependency Inversion Principle):一种软件架构设计的原则(抽象概念)。

    控制反转(IoC)(Inversion of Control):一种反转流、依赖和接口的方式(DIP的具体实现方式)。

    依赖注入(DI)(Dependency Injection):IoC的一种实现方式,用来反转依赖(IoC的具体实现方式)。

    IoC容器 :依赖注入的框架,用来映射依赖,管理对象创建和生存周期(DI框架)。

    依赖倒置原则


    依赖倒置原则,它转换了依赖,高层模块不依赖于低层模块的实现,而低层模块依赖于高层模块定义的接口。通俗的讲,就是高层模块定义接口,低层模块负责实现。

    Bob Martins对DIP的定义:
    高层模块不应依赖于低层模块,两者应该依赖于抽象。
    抽象不应该依赖于实现,实现应该依赖于抽象。

    场景一 依赖无倒置(低层模块定义接口,高层模块负责实现)


    从上图中,我们发现高层模块的类依赖于低层模块的接口。因此,低层模块需要考虑到所有的接口。如果有新的低层模块类出现时,高层模块需要修改代码,来实现新的低层模块的接口。这样,就破坏了开放封闭原则。

    场景二 依赖倒置(高层模块定义接口,低层模块负责实现)

    在这个图中,我们发现高层模块定义了接口,将不再直接依赖于低层模块,低层模块负责实现高层模块定义的接口。这样,当有新的低层模块实现时,不需要修改高层模块的代码。

    由此,我们可以总结出使用DIP的优点:

    系统更柔韧:可以修改一部分代码而不影响其他模块。

    系统更健壮:可以修改一部分代码而不会让系统崩溃。

    系统更高效:组件松耦合,且可复用,提高开发效率。

    控制反转 (IoC)


    DIP是一种 软件设计原则,它仅仅告诉你两个模块之间应该如何依赖,但是它并没有告诉如何做。IoC则是一种 软件设计模式,它告诉你应该如何做,来解除相互依赖模块的耦合。控制反转(IoC),它为相互依赖的组件提供抽象,将依赖(低层模块)对象的获得交给第三方(系统)来控制,即依赖对象不在被依赖模块的类中直接通过new来获取。

    软件设计原则:原则为我们提供指南,它告诉我们什么是对的,什么是错的。它不会告诉我们如何解决问题。它仅仅给出一些准则,以便我们可以设计好的软件,避免不良的设计。一些常见的原则,比如DRY、OCP、DIP等。

    软件设计模式:模式是在软件开发过程中总结得出的一些可重用的解决方案,它能解决一些实际的问题。一些常见的模式,比如工厂模式、单例模式等等。

    IoC有2种常见的实现方式:依赖注入和服务定位。主要的实现方式依赖注入。

    依赖注入 (DI)


    控制反转(IoC)一种重要的方式,就是将依赖对象的创建和绑定转移到被依赖对象类的外部来实现。

    依赖注入(DI),它提供一种机制,将需要依赖(低层模块)对象的引用传递给被依赖(高层模块)对象。

    php中传递依赖的方法

    方法一 直接在高层模块new 底层模块的类

     class Bim
        {
            public function doSomething()
            {
                echo __METHOD__, '|';
            }
        }
        
        class Bar
        {
            public function doSomething()
            {
                $bim = new Bim();
                $bim->doSomething();
                echo __METHOD__, '|';
            }
        }
        
        class Foo
        {
            public function doSomething()
            {
                $bar = new Bar();
                $bar->doSomething();
                echo __METHOD__;
            }
        }
        
        $foo = new Foo();
        $foo->doSomething(); //Bim::doSomething|Bar::doSomething|Foo::doSomething
    

    缺点:类与类之间的耦合程度太高,Foo必须依赖Bar,没有Bar,Foo就不能工作,这种方案最不可取。

    方法二 构造函数注入

    构造函数函数注入,毫无疑问通过构造函数传递依赖。因此,构造函数的参数必然用来接收一个依赖对象。那么参数的类型是什么呢?具体依赖对象的类型?还是一个抽象类型?根据DIP原则,我们知道高层模块不应该依赖于低层模块,两者应该依赖于抽象。那么构造函数的参数应该是一个抽象类型。

     class Bim
        {
            public function doSomething()
            {
                echo __METHOD__, '|';
            }
        }
        
        class Bar
        {
            private $bim;
        
            public function __construct(Bim $bim)
            {
                $this->bim = $bim;
            }
        
            public function doSomething()
            {
                $this->bim->doSomething();
                echo __METHOD__, '|';
            }
        }
        
        class Foo
        {
            private $bar;
        
            public function __construct(Bar $bar)
            {
                $this->bar = $bar;
            }
        
            public function doSomething()
            {
                $this->bar->doSomething();
                echo __METHOD__;
            }
        }
        
        $foo = new Foo(new Bar(new Bim()));
        $foo->doSomething(); // Bim::doSomething|Bar::doSomething|Foo::doSomething
    

    缺点:如果依赖过多,那么在构造方法里必然传入多个参数,三个以上就会使代码变的难以阅读。

    方法三 setter注入

    相比构造函数注入,setter注入显得有些复杂,使用也不常见。具体思路是先定义一个接口,包含一个设置依赖的方法。然后依赖类,继承并实现这个接口。

    /**
     * 接口
     */
    interface IDeviceWriter
    {
        public function saveToDevice();
    }
    
    /**
     * 高层
     */
    class Business
    {
        /**
         * @var IDeviceWriter
         */
        private $writer;
    
        /**
         * @param IDeviceWriter $writer
         */
        public function setWriter($writer)
        {
            $this->writer = $writer;
        }
    
        public function save()
        {
            $this->writer->saveToDevice();
        }
    }
    
    /**
     * 低层,软盘存储
     */
    class FloppyWriter implements IDeviceWriter
    {
    
        public function saveToDevice()
        {
            echo __METHOD__;
        }
    }
    
    /**
     * 低层,USB盘存储
     */
    class UsbDiskWriter implements IDeviceWriter
    {
    
        public function saveToDevice()
        {
            echo __METHOD__;
        }
    }
    
    $biz = new Business();
    $biz->setWriter(new UsbDiskWriter());
    $biz->save(); // UsbDiskWriter::saveToDevice
    
    $biz->setWriter(new FloppyWriter());
    $biz->save(); // FloppyWriter::saveToDevice
    

    缺点:同样存在和第二种方案一样的弊端,当依赖的类增多时,我们需要些很多很多的set方法。

    基于以上的缺点,这时我们在想如果有一个专门的类(或者说一个容器)可以帮我们管理这些依赖关系就好了。

    方法四 IoC容器注入

    对于大型项目来说,相互依赖的组件比较多。如果还用手动的方式,自己来创建和注入依赖的话,显然效率很低,而且往往还会出现不可控的场面。正因如此,IoC容器诞生了。IoC容器实际上是一个DI框架,它能简化我们的工作量。它包含以下几个功能:

    • 管理应用程序中的『全局』对象(包括 动态创建、注入依赖对象、管理对象生命周期、映射依赖关系)
    • 可以延时加载对象(仅用到时才创建对象)
    • 促进编写可重用、可测试和松耦合的代码
    class SomeComponent
    {
    
        protected $_di;
    
        public function __construct($di)
        {
            $this->_di = $di;
        }
    
        public function someDbTask()
        {
    
            // 获得数据库连接实例
            // 总是返回一个新的连接
            $connection = $this->_di->get('db');
    
        }
    
        public function someOtherDbTask()
        {
    
            // 获得共享连接实例
            // 每次请求都返回相同的连接实例
            $connection = $this->_di->getShared('db');
    
            // 这个方法也需要一个输入过滤的依赖服务
            $filter = $this->_di->get('filter');
    
        }
    
    }
    
    $di = new PhalconDI();
    
    //在容器中注册一个db服务
    $di->set('db', function() {
        return new Connection(array(
            "host" => "localhost",
            "username" => "root",
            "password" => "secret",
            "dbname" => "invo"
        ));
    });
    
    //在容器中注册一个filter服务
    $di->set('filter', function() {
        return new Filter();
    });
    
    //在容器中注册一个session服务
    $di->set('session', function() {
        return new Session();
    });
    
    //把传递服务的容器作为唯一参数传递给组件
    $some = new SomeComponent($di);
    $some->someTask();
    

    这个组件现在可以很简单的获取到它所需要的服务,服务采用延迟加载的方式(因为注册的时候,都是传入了一个匿名函数,只有在需要使用的时候才初始化),这也节省了服务器资源。这个组件现在是高度解耦。例如,我们可以替换掉创建连接的方式,它们的行为或它们的任何其他方面,也不会影响该组件。

    参考资料

    原则&模式|理解DIP、IoC、DI以及IoC容器
    PHP程序员如何理解IoC/DI
    PHP程序员如何理解依赖注入容器(dependency injection container)
    Laravel 依赖注入思想
    php依赖注入

  • 相关阅读:
    vs编译出现 fatal error LNK1281:无法生成 SAFESEH 映像
    $apply()和$digest()——angular
    JS获取URL中参数值
    NeDB——node嵌入式数据库
    VS Code常用插件
    js断点调试
    VS Code 使用Git进行版本控制
    VS Code快捷键
    用户tokenId
    node-webkit-updater——NW.js自动更新
  • 原文地址:https://www.cnblogs.com/mingaixin/p/6278560.html
Copyright © 2011-2022 走看看