当前位置:网站首页>编程哲学——自动加载、依赖注入与控制反转

编程哲学——自动加载、依赖注入与控制反转

2022-04-23 14:35:00 云闲不收

知识前导

需要先了解java import、package的实质或php的namespace和use实质。它们不是文件路径,而只是为了避免类属性的冲突。文件需要通过javapath或者php的require引入方能使用。

类的自动加载

问题引入

当我们写代码的时候,经常需要先引入对应的类文件,这样当我们频繁的引入其他文件使用其方法时就很麻烦。为此,我们希望,当我们实例化某类的时候,代码能够帮我们自动引入对应的文件

实现

// Goods.php
<?php
class Goods
{
    
    public function getGoods()
    {
    
        return 'phone';
    }
}

在PHP开发过程中,如果希望从外部引入一个class,通常会使用include和require方法,去把定义这个class的文件包含进来,但是这样可能会使得在引用文件的新脚本中,存在大量的include或require方法调用,如果一时疏忽遗漏则会产生错误,使得代码难以维护。
自PHP5后,引入了autoload这个拦截器方法

// test_autoload.php
function __autoload($class_name = "") {
    
    echo 'class:' . $class_name . PHP_EOL;
    include "./{
      $class_name}.php";
}

$goods = new Goods();
echo $goods->getGoods();

如此 我们就不需要再手动引入文件了。

而且现在很多IDE很智能,比如PHPSTORM 当我们敲$a=new TestMgr();时,ide会自动在文件上方帮我们写use app\api\manager\TestMgr;

use app\api\manager\TestMgr;//ide自动帮我们添加

class Test
{
    
    public function index()
    {
    
        $a=new TestMgr();
    }
}

依赖注入

问题引入

当我们使用自动加载后,写代码大大方便了起来。但是我们后来发现,如果我们编程技术增长后,某天突然觉得之前的代码很乱,想要重构代码,比如将原来零散的微信登录、微信发送服务号消息、微信小程序登录等微信官方的api调用方法集合到一起合成一个单独的类WeChatService,这时候你就发现头疼了,因为你得一个一个的把之前的use xxx 和代码中无数的new xxx()给修改过来。

场景引入

第一章:小明和他的手机

从前有个人叫小明
小明有三大爱好,抽烟,喝酒…… 咳咳,不好意思,走错片场了。应该是逛知乎、玩王者农药和抢微信红包
我们用一段简单的伪代码,来制造一个这样的小明

class Ming extends Person
{
    
    private $_name;

    private $_age;

    function read()
    {
    
        //逛知乎
    }

    function  play()
    {
    
        //玩农药
    }

    function  grab()
    {
    
        //抢红包
    }

}

但是,小明作为一个人类,没有办法仅靠自己就能实现以上的功能,他必须依赖一部手机,所以他买了一台iphone6,接下来我们来制造一个iphone6

class iPhone6 extends Iphone
{
    
    function read($user="某人")
    {
    
        echo $user."打开了知乎然后编了一个故事 \n";
    }

    function play($user="某人")
    {
    
        echo $user."打开了王者农药并送起了人头 \n";
    }

    function grab($user="某人")
    {
    
        echo $user."开始抢红包却只抢不发 \n";
    }
}

小明非常珍惜自己的新手机,每天把它牢牢控制在手心里,所以小明变成了这个样子

class Ming extends Person
{
    
    private $_name;

    private $_age;

    public function  __construct()
    {
    
        $this->_name = '小明';
        $this->_age = 26;
    }

    function read()
    {
    
        //…… 省略若干代码
        (new iPhone6())->read($this->_name); //逛知乎
    }

    function  play()
    {
    
        //…… 省略若干代码
        (new iPhone6())->play($this->_name);//玩农药

    }

    function  grab()
    {
    
        //…… 省略若干代码
        (new iPhone6())->grab($this->_name);//抢红包

    }

}

今天是周六,小明不用上班,于是他起床,并依次逛起了知乎,玩王者农药,并抢了个红包。

$ming = new Ming();  //小明起床
$ming->read();
$ming->play();
$ming->grab();

这个时候,我们可以在命令行里看到输出如下

小明打开了知乎然后编了一个故事 
小明打开了王者农药并送起了人头 
小明开始抢红包却只抢不发

这一天,小明过得很充实,他觉得自己是世界上最幸福的人。

第二章: 小明的快乐与忧伤

小明和他的手机曾一起度过了一段美好的时光,一到空闲时刻,他就抱着手机,逛知乎,刷微博,玩游戏,他觉得自己根本不需要女朋友,只要有手机在身边,就满足了。

可谁能想到,一次次地系统更新彻底打碎了他的梦想,他的手机变得越来越卡顿,电池的使用寿命也越来越短,一直到某一天的寒风中,他的手机终于耐不住寒冷,头也不回地关了机。

小明很忧伤,他意识到,自己要换手机了。

为了能获得更好的使用体验,小明一咬牙,剁手了一台iphoneX,这部手机铃声很大,电量很足,还能双卡双待,小明很喜欢,但是他遇到一个问题,就是他之前过度依赖了原来那一部iPhone6,他们之间已经深深耦合在一起了,如果要换手机,他就要拿起刀来改造自己,把自己体内所有方法中的iphone6 都换成 iphoneX。
在这里插入图片描述

漫长的改造过程

经历了漫长的改造过程,小明终于把代码中的 iphone6 全部换成了 iphoneX。虽然很辛苦,但是小明觉得他是快乐的。

于是小明开开心心地带着手机去上班了,并在回来的路上被小偷偷走了。为了应急,小明只好重新使用那部刚刚被遗弃的iphone6,但是一想到那漫长的改造过程,小明的心里就说不出的委屈,他觉得自己过于依赖手机了,为什么每次手机出什么问题他都要去改造他自己,这不仅仅是过度耦合,简直是本末倒置,他向天空大喊,我不要再控制我的手机了。

天空中的造物主,也就是作为程序员的我,听到了他的呐喊,我告诉他,你不用再控制你的手机了,交给我来管理,把控制权交给我。这就叫做控制反转。

第三章:造物主的智慧

小明听到了我的话,他既高兴,又有一点害怕,他跪下来磕了几个头,虔诚地说到:“原来您就是传说中的造物主,巴格梅克上神。我听到您刚刚说了 控制反转 四个字,就是把手机的控制权从我的手里交给你,但这只是您的想法,是一种思想罢了,要用什么办法才能实现控制反转,又可以让我继续使用手机呢?”

“呵“,身为造物主的我在表现完不屑以后,扔下了四个大字,“依赖注入!”

接下来,伟大的我开始对小明进行惨无人道的改造,如下

class Ming extends Person
{
    
    private $_name;
    private $_age;

    private $_phone; //将手机作为自己的成员变量

    public function  __construct($phone)
    {
    
        $this->_name = '小明';
        $this->_age = 26;
        $this->_phone = $phone;
        echo "小明起床了 \n";
    }

    function read()
    {
    
        //…… 省略若干代码
        $this->_phone->read($this->_name); //逛知乎
    }

    function  play()
    {
    
        //…… 省略若干代码
        $this->_phone->play($this->_name);//玩农药

    }

    function  grab()
    {
    
        //…… 省略若干代码
        $this->_phone->grab($this->_name);//抢红包

    }

}

接下来,我们来模拟运行小明的一天

$phone = new IphoneX(); //创建一个iphoneX的实例
if($phone->isBroken()){
    //如果iphone不可用,则使用旧版手机
    $phone = new Iphone6();
}
$ming = new Ming($phone);//小明不用关心是什么手机,他只要玩就行了。
$ming->read();
$ming->play();
$ming->grab();

我们先看一下iphoneX 是否可以使用,如果不可以使用,则直接换成iphone6,然后唤醒小明,并把手机塞到他的手里,换句话说,把他所依赖的手机直接注入到他的身上,他不需要关心自己拿的是什么手机,他只要直接使用就可以了。

这就是依赖注入。

第四章:小明的感悟

小明的生活开始变得简单了起来,而他把省出来的时间都用来写笔记了,他在笔记本上这样写到

我曾经有很强的控制欲,过度依赖于我的手机,导致我和手机之间耦合程度太高,只要手机出现一点点问题,我都要改造我自己,这实在是既浪费时间又容易出问题。自从我把控制权交给了造物主,他每天在唤醒我以前,就已经替我选好了手机,我只要按照平时一样玩手机就可以了,根本不用关心是什么手机。即便手机出了问题,也可以由造物主直接搞定,不需要再改造我自己了,我现在买了七部手机,都交给了造物主,每天换一部,美滋滋!
我也从其中获得了这样的感悟: 如果一个类A 的功能实现需要借助于类B,那么就称类B是类A的依赖,如果在类A的内部去实例化类B,那么两者之间会出现较高的耦合,一旦类B出现了问题,类A也需要进行改造,如果这样的情况较多,每个类之间都有很多依赖,那么就会出现牵一发而动全身的情况,程序会极难维护,并且很容易出现问题。要解决这个问题,就要把A类对B类的控制权抽离出来,交给一个第三方去做,把控制权反转给第三方,就称作控制反转(IOC Inversion Of Control)。控制反转是一种思想,是能够解决问题的一种可能的结果,而依赖注入(Dependency Injection)就是其最典型的实现方法。由第三方(我们称作IOC容器)来控制依赖,把他通过构造函数、属性或者工厂模式等方法,注入到类A内,这样就极大程度的对类A和类B进行了解耦。

第五章 小明的困惑

有一天,小明发现自己在想阅读知乎的时候,读到了这样一行文字。

未完待续…

以上引自 https://zhuanlan.zhihu.com/p/33492169

注释:上面的场景只解释了为什么要依赖注入,而没讲很多的依赖反转。我们可以发现 上面的例子 还存在问题 1是需要我们手动去new一个具体的对象 我们想要更懒一点 不自己手动写 2是如果我们有很多类嵌套 不是要new很多类么

控制反转

定义概念初探

当我们不止一个类 可能A B C D 可能A依赖B C ,C又依赖于D类,这样就搞的关系很复杂,所以依赖反转产生了
在这里插入图片描述
依赖倒置:
在这里插入图片描述

控制反转的应用实例(java实现 讲的很好!)

为了理解这几个概念,我们还是用上面汽车的例子。只不过这次换成代码。我们先定义四个Class,车,车身,底盘,轮胎。然后初始化这辆车,最后跑这辆车。代码结构如下:
在这里插入图片描述
这样,就相当于上面第一个例子,上层建筑依赖下层建筑——每一个类的构造函数都直接调用了底层代码的构造函数。假设我们需要改动一下轮胎(Tire)类,把它的尺寸变成动态的,而不是一直都是30。我们需要这样改:
在这里插入图片描述
由于我们修改了轮胎的定义,为了让整个程序正常运行,我们需要做以下改动:
在这里插入图片描述
由此我们可以看到,仅仅是为了修改轮胎的构造函数,这种设计却需要修改整个上层所有类的构造函数!
上述的例子还可以改成普通函数的情况,底层的数据库层需要多加一个参数,比如活跃时间,那么从最上层开始,每个调用了这个函数的 都要改一遍!
在软件工程中,这样的设计几乎是不可维护的——在实际工程项目中,有的类可能会是几千个类的底层,如果每次修改这个类,我们都要修改所有以它作为依赖的类,那软件的维护成本就太高了。
所以我们需要进行控制反转(IoC),及上层控制下层,而不是下层控制着上层。我们用依赖注入(Dependency Injection)这种方式来实现控制反转。所谓依赖注入,就是把底层类作为参数传入上层类,实现上层类对下层类的“控制”。这里我们用构造方法传递的依赖注入方式重新写车类的定义:
在这里插入图片描述
这里我们再把轮胎尺寸变成动态的,同样为了让整个系统顺利运行,我们需要做如下修改:
在这里插入图片描述
看到没?这里我只需要修改轮胎类就行了,不用修改其他任何上层类。这显然是更容易维护的代码。不仅如此,在实际的工程中,这种设计模式还有利于不同组的协同合作和单元测试:比如开发这四个类的分别是四个不同的组,那么只要定义好了接口,四个不同的组可以同时进行开发而不相互受限制;而对于单元测试,如果我们要写Car类的单元测试,就只需要Mock一下Framework类传入Car就行了,而不用把Framework, Bottom, Tire全部new一遍再来构造Car。

这里我们是采用的构造函数传入的方式进行的依赖注入。其实还有另外两种方法:Setter传递和接口传递。这里就不多讲了,核心思路都是一样的,都是为了实现控制反转。
在这里插入图片描述
看到这里你应该能理解什么控制反转和依赖注入了。那什么是控制反转容器(IoC Container)呢?其实上面的例子中,对车类进行初始化的那段代码发生的地方,就是控制反转容器。
显然你也应该观察到了,因为采用了依赖注入,**在初始化的过程中就不可避免的会写大量的new。而我们真正关心的参数只有一个,那就是new Car(40) 其他都是固定写法 **这里IoC容器就解决了这个问题。这个容器可以自动对你的代码进行初始化,你只需要维护一个Configuration(可以是xml可以是一段代码),而不用每次初始化一辆车都要亲手去写那一大段初始化的代码。这是引入IoC Container的第一个好处。

IoC Container的第二个好处是:我们在创建实例的时候不需要了解其中的细节。在上面的例子中,我们自己手动创建一个车instance时候,是从底层往上层new的:
在这里插入图片描述
这个过程中,我们需要了解整个Car/Framework/Bottom/Tire类构造函数是怎么定义的,才能一步一步new/注入。

而IoC Container在进行这个工作的时候是反过来的,它先从最上层开始往下找依赖关系,到达最底层之后再往上一步一步new(有点像深度优先遍历):
在这里插入图片描述
在这里插入图片描述
我们就像是工厂的客户。我们只需要向工厂请求一个Car实例,然后它就给我们按照Config创建了一个Car实例。我们完全不用管这个Car实例是怎么一步一步被创建出来。

实际项目中,有的Service Class可能是十年前写的,有几百个类作为它的底层。假设我们新写的一个API需要实例化这个Service,我们总不可能回头去搞清楚这几百个类的构造函数吧?IoC Container的这个特性就很完美的解决了这类问题——因为这个架构要求你在写class的时候需要写相应的Config文件,所以你要初始化很久以前的Service类的时候,前人都已经写好了Config文件,你直接在需要用的地方注入这个Service就可以了。这大大增加了项目的可维护性且降低了开发难度。
Spring中的依赖注入

上面我们提到,依赖注入是实现控制反转的一种方式。下面我们结合Spring的IoC容器,简单描述一下这个过程。

class MovieLister...
    private MovieFinder finder;
    public void setFinder(MovieFinder finder) {
    
        this.finder = finder;
    }
class ColonMovieFinder...
    public void setFilename(String filename) {
    
        this.filename = filename;
    }

我们先定义两个类,可以看到都使用了依赖注入的方式,通过外部传入依赖,而不是自己创建依赖。那么问题来了,谁把依赖传给他们,也就是说谁负责创建finder,并且把finder传给MovieLister。答案是Spring的IoC容器。要使用IoC容器,首先要进行配置。这里我们使用xml的配置,也可以通过代码注解方式配置。下面是spring.xml的内容

<beans>
    <bean id="MovieLister" class="spring.MovieLister">
        <property name="finder">
            <ref local="MovieFinder"/>
        </property>
    </bean>
    <bean id="MovieFinder" class="spring.ColonMovieFinder">
        <property name="filename">
            <value>movies1.txt</value>
        </property>
    </bean>
</beans>

在Spring中,每个bean代表一个对象的实例,默认是单例模式,即在程序的生命周期内,所有的对象都只有一个实例,进行重复使用。通过配置bean,IoC容器在启动的时候会根据配置生成bean实例。具体的配置语法参考Spring文档。这里只要知道IoC容器会根据配置创建MovieFinder,在运行的时候把MovieFinder赋值给MovieLister的finder属性,完成依赖注入的过程。下面给出测试代码

public void testWithSpring() throws Exception {
    
    ApplicationContext ctx = new FileSystemXmlApplicationContext("spring.xml");//1
    MovieLister lister = (MovieLister) ctx.getBean("MovieLister");//2
    Movie[] movies = lister.moviesDirectedBy("Sergio Leone");
    assertEquals("Once Upon a Time in the West", movies[0].getTitle());
}

根据配置生成ApplicationContext,即IoC容器。
从容器中获取MovieLister的实例。
总结

控制反转是一种在软件工程中解耦合的思想,调用类只依赖接口,而不依赖具体的实现类,减少了耦合。控制权交给了容器,在运行的时候才由容器决定将具体的实现动态的“注入”到调用类的对象中。
依赖注入是一种设计模式,可以作为控制反转的一种实现方式。依赖注入就是将实例变量传入到一个对象中去(Dependency injection means giving an object its instance variables)。
通过IoC框架,类A依赖类B的强耦合关系可以在运行时通过容器建立,也就是说把创建B实例的工作移交给容器,类A只管使用就可以。

以上转自 https://zhuanlan.zhihu.com/p/77415657 原作者已不可查

简单总结(依赖注入和控制反转)

实际上这种很像接口!在强类型的语言中,必须要对参数进行声明,而这个类不能是普通的类 所以也就变成了接口。 __construt(Phone $phone) ,iphone6和iphonex都是它的实现类

大多数面向对象编程语言,在调用一个类的时候,先要实例化这个类,生成一个对象。如果你在写一个类,过程中要调用到很多其它类,甚至这里的其它类,也要“依赖”于更多其它的类,那么可以想象,你要进行多少次实例化。这就是“依赖”的意思。依赖注入,全称是“依赖注入到容器”, 容器(IOC容器)是一个设计模式,它也是个对象,你把某个类(不管有多少依赖关系)放入这个容器中,可以“解析”出这个类的实例。所以依赖注入就是把有依赖关系的类放入容器(IOC容器)中,然后解析出这个类的实例。仅此而已。

本来我接受各种参数来构造一个对象,现在只接受一个参数——已经实例化的对象。
也就是说我对对象的『依赖是注入进来的』,而和它的构造方式解耦了。构造和销毁这些『控制』操作也交给了第三方,也就是『控制反转』。

代码现在只管实现逻辑,new 对象的任务交给 IoC 容器去做。

在这里插入图片描述

实现–PHP

依赖注入的实现

依赖注入(DI)的概念虽然听起来很深奥,但是如果你用过一些新兴的php框架的话,对于DI一定不陌生,因为它们多多少少都用到了依赖注入来处理类与类之间的依赖关系。

php中传递依赖关系的三种方案
其实要理解DI,首先要明白在php中如何传递依赖关系。

第一种方案,也是最不可取的方案,就是在A类中直接用new关键词来创建一个B类,如下代码所示:

<?php
class A
{
    
  public function __construct()
  {
    
    $b = new B();//a要依赖b的方法 那么就自己手动创建一个B 实际上是小明第一种写法的方式
    //这里写在构造器 是将其统一到一个地方管理了 不用到处去写
  }
}

为什么这种方案不可取呢?因为这样的话,A与B就耦合在了一起,也就是说A类无法脱离B类工作

第二种方案就是在A类的方法中传入需要的B类,如下代码所示:

class A
{
    
  public function __construct(B $b)
  {
    
  		$this->b = $b;
  		//好处在于 1 A类不用和B类绑定到一起
  		//2 是函数外部手动传入依赖的B的实例
  		//3 是将其统一到一个地方管理了 不用到处去写
  }
}

这种方法比第一种方案有了改进,A类不必与B类捆绑在一起,只要传入的类满足A类的需求,也可以是C类,也可以是D类等等。
但是这种方案的弊端在于如果A类依赖的类较多,参数列表会很长,容易发生混乱。

第三种方案是使用set方法传入,如下代码所示:
这种方法比第一种方案有了改进,A类不必与B类捆绑在一起,只要传入的类满足A类的需求,也可以是C类,也可以是D类等等。 如下代码所示:

class A
{
    
  public function setB(B $b)
  {
    
    $this->b = $b;
  }
}

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

ps 这里是广度的增多 而java那里 是深度的增多

控制反转的实现——一个专门的类,或者说一个容器去管理

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

控制反转之统一管理依赖注入

一个简单的依赖注入的例子
如下代码来自twittee:

// Container类在外部 一个统一集中的地方去管理这些依赖关系
class Container {
    
 private $s=array();//注意 这里采用了数组的形式 就可以避免很长的参数列表
 function __set($k, $c) {
     
 	$this->s[$k]=$c;  //k就是给类取的新名字 
 }
 function __get($k) {
     //S就是这个类现在的名字(注意 不单单是重起了名字 还实例化了)
 	return $this->s[$k]($this); 
 }
}

有了container类之后我们可以怎样管理A与B之间的依赖关系呢,用代码说话吧:

<?php
class A
{
    
  private $container;
  //只需要container一个参数了
  public function __construct(Container $container)
  {
    
    $this->container = $container;
  }
  public function doSomeThing()
  {
    
    //do something which needs class B
    $b = $this->container->getB();
    //to do
  }
}

再将B类注入到容器类中:

$c = new Container();
$c->setB(new B());

控制反转之 ----控制实例化

还可以传入一个匿名函数,这样B类就不会在传入时就立即实例化,而是在真正调用时才完成实例化的工作


$c = new Container();
$c->setB(function (){
    
  return new B();
});

实现代码出自https://zhuanlan.zhihu.com/p/365923544

讲的不如java那里的好 但是有个需要的时候才实例化 以及数组管理的思想 写的不错

一句话总结

依赖注入就是,A依赖类B 以前A自己newB 现在让调用者 new好了B的实例直接传给A。这样两个好处:1 好修改 2 相当于自己对类命新名字了

控制反转就是 由于A不止依赖一个B类,如果要我们自己去调用 我们需要去自己查所有的依赖关系,很麻烦。所以,让调用者 new好了A需要的一系列实例这一工作,交给专门的一个类去集中管理,这就是控制反转(写底层类的人的时候就定义好的)

tp6自动绑定与自动实例化的意思

<?php
class A
{
    
  public function __construct(B $b)
  {
    
  		$this->b = $b;
  }
}

当我们采用这种形式的时候,要使用A类,必须要手动先new一个B类 ,比如之前的

$phone = new IphoneX(); //创建一个iphoneX的实例
$ming = new Ming($phone);//小明不用关心是什么手机,他只要玩就行了。

但是,我们希望更简洁,只要new Ming()就好。所以$phone= new IphoneX这个叫自动实例化 ,而我们只想new Ming() 把Phone的实例自动搞进去 就叫自动绑定?

什么时候使用

当我们写的类(我们叫中间层类)使用了其他类的方法,而我们的中间层类本身也可能被上层类调用的时候
第一种方法是,我们手动去new 下层类,而这会造成上述各种问题。
第二种方法是,将下层类的对象变成自己的参数,然后new交给上层函数(先不考虑容器)。
优先使用第二种方法。new的事情找个有容器的框架去做就行

底层的数据库层需要多加一个参数,比如活跃时间,那么从最上层开始,每个调用了这个函数的 都要改一遍!这时候就一定考虑依赖注入!

怎么实现控制反转—通过反射

简而言之 就是通过反射 一层一层实例化

PHP具有完整的反射 API,提供了对类、接口、函数、方法和扩展进行逆向工程的能力。通过类的反射提供的能力我们能够知道类是如何被定义的,它有什么属性、什么方法、方法都有哪些参数,类文件的路径是什么等很重要的信息。也正式因为类的反射很多PHP框架才能实现依赖注入自动解决类与类之间的依赖关系,这给我们平时的开发带来了很大的方便。 本文主要是讲解如何利用类的反射来实现依赖注入(Dependency Injection),并不会去逐条讲述PHP Reflection里的每一个API,详细的API参考信息请查阅官方文档

为了更好地理解,我们通过一个例子来看类的反射,以及如何实现依赖注入。
下面这个类代表了坐标系里的一个点,有两个属性横坐标x和纵坐标y。

/** * Class Point */
class Point
{
    
  public $x;
  public $y;
 
  /** * Point constructor. * @param int $x horizontal value of point's coordinate * @param int $y vertical value of point's coordinate */
  public function __construct($x = 0, $y = 0)
  {
    
    $this->x = $x;
    $this->y = $y;
  }
}

接下来这个类代表圆形,可以看到在它的构造函数里有一个参数是Point类的,即Circle类是依赖与Point类的。

class Circle
{
    
  /** * @var int */
  public $radius;//半径
 
  /** * @var Point */
  public $center;//圆心点
 
  const PI = 3.14;
 
  public function __construct(Point $point, $radius = 1)
  {
    
    $this->center = $point;
    $this->radius = $radius;
  }
   
  //打印圆点的坐标
  public function printCenter()
  {
    
    printf('center coordinate is (%d, %d)', $this->center->x, $this->center->y);
  }
 
  //计算圆形的面积
  public function area()
  {
    
    return 3.14 * pow($this->radius, 2);
  }
}

ReflectionClass

下面我们通过反射来对Circle这个类进行反向工程。
把Circle类的名字传递给reflectionClass来实例化一个ReflectionClass类的对象。

$reflectionClass = new reflectionClass(Circle::class);
//返回值如下
object(ReflectionClass)#1 (1) {
    
 ["name"]=>
 string(6) "Circle"
}

反射出类的常量

$reflectionClass->getConstants();

返回一个由常量名称和值构成的关联数组

array(1) {
    
 ["PI"]=>
 float(3.14)
}

通过反射获取属性

$reflectionClass->getProperties();

返回一个由ReflectionProperty对象构成的数组

array(2) {
    
 [0]=>
 object(ReflectionProperty)#2 (2) {
    
  ["name"]=>
  string(6) "radius"
  ["class"]=>
  string(6) "Circle"
 }
 [1]=>
 object(ReflectionProperty)#3 (2) {
    
  ["name"]=>
  string(6) "center"
  ["class"]=>
  string(6) "Circle"
 }
}

反射出类中定义的方法

$reflectionClass->getMethods();

返回ReflectionMethod对象构成的数组

array(3) {
    
 [0]=>
 object(ReflectionMethod)#2 (2) {
    
  ["name"]=>
  string(11) "__construct"
  ["class"]=>
  string(6) "Circle"
 }
 [1]=>
 object(ReflectionMethod)#3 (2) {
    
  ["name"]=>
  string(11) "printCenter"
  ["class"]=>
  string(6) "Circle"
 }
 [2]=>
 object(ReflectionMethod)#4 (2) {
    
  ["name"]=>
  string(4) "area"
  ["class"]=>
  string(6) "Circle"
 }
}

我们还可以通过getConstructor()来单独获取类的构造方法,其返回值为一个ReflectionMethod对象。

$constructor = $reflectionClass->getConstructor();

反射出方法的参数

$parameters = $constructor->getParameters();

其返回值为ReflectionParameter对象构成的数组。

array(2) {
    
 [0]=>
 object(ReflectionParameter)#3 (1) {
    
  ["name"]=>
  string(5) "point"
 }
 [1]=>
 object(ReflectionParameter)#4 (1) {
    
  ["name"]=>
  string(6) "radius"
 }
}

依赖注入

好了接下来我们编写一个名为make的函数,传递类名称给make函数返回类的对象,在make里它会帮我们注入类的依赖,即在本例中帮我们注入Point对象给Circle类的构造方法。

//构建类的对象
function make($className)
{
    
  $reflectionClass = new ReflectionClass($className);
  $constructor = $reflectionClass->getConstructor();
  $parameters = $constructor->getParameters();
  $dependencies = getDependencies($parameters);
   
  return $reflectionClass->newInstanceArgs($dependencies);
}
 
//依赖解析
function getDependencies($parameters)
{
    
  $dependencies = [];
  foreach($parameters as $parameter) {
    
    $dependency = $parameter->getClass();
    if (is_null($dependency)) {
    
      if($parameter->isDefaultValueAvailable()) {
    
        $dependencies[] = $parameter->getDefaultValue();
      } else {
    
        //不是可选参数的为了简单直接赋值为字符串0
        //针对构造方法的必须参数这个情况
        //laravel是通过service provider注册closure到IocContainer,
        //在closure里可以通过return new Class($param1, $param2)来返回类的实例
        //然后在make时回调这个closure即可解析出对象
        //具体细节我会在另一篇文章里面描述
        $dependencies[] = '0';
      }
    } else {
    
      //递归解析出依赖类的对象
      $dependencies[] = make($parameter->getClass()->name);
    }
  }
 
  return $dependencies;
}

定义好make方法后我们通过它来帮我们实例化Circle类的对象:

$circle = make('Circle');
$area = $circle->area();
/*var_dump($circle, $area); object(Circle)#6 (2) { ["radius"]=> int(1) ["center"]=> object(Point)#11 (2) { ["x"]=> int(0) ["y"]=> int(0) } } float(3.14)*/

如果通过正常的调用流程

new point
new circle
然后调用方法 

通过上面这个实例我简单描述了一下如何利用PHP类的反射来实现依赖注入,Laravel的依赖注入也是通过这个思路来实现的,只不过设计的更精密大量地利用了闭包回调来应对各种复杂的依赖注入。

来源:https://www.jb51.net/article/134530.htm

问题关联

thinkphp怎么自动引入的文件

其他问题

上面好像是自动完成的 没法自定义传参?如果比如要修改两个点的距离怎么搞呢。
我猜测 由于web开发 一般类只是用来调用方法的 不像普通的java那样 set get 比如new(“张三”,“中国人”) 所以用这种可以实例化 不需要传参。
在这里插入图片描述
比如CI框架的load方法 实质就是实例化一个对象。但是 如果你要加一层service,你还容易改写loader类,比如让它也搜索service目录去找。但是 这样就失去了精准定位 变成了搜索文件!特别分层一多 那不是emmm。当然还有其他方法 比如this->load->service。但是这样,如果分层多了 那不是也得定义好多个。 再或者 你可以通过后缀名去自动匹配目录(这个好像还挺好的?还可以写个目录反射 什么什么后缀对应什么什么目录)-》但是这样会出现依赖注入那个问题 比如由于服务号和小程序推送消息 你想部分改用公众号推送(实现逻辑复杂 不能光一个函数 要写一个类) 这时候去改 ----额 没想好

工厂模式和依赖注入

场景引入

第一步:为什么我们不能new对象?

随时随地new对象会引发一个问题,就是你new的实例会满天飞。这样一来你每一个实体进行扩充,修改,放弃的时候你就需要满世界的找对象,一个一个改过来。
例如:
collection map=new HashMap();
有一天老大跟你说 HashMap不太行,全部换成新版的XXXXMap吧~~~你是不是需要全部一起换?
所以这时候我们需要通过工厂模式来管理对象了。代码如下:
collection map=MapFactory.createMap();
这样一来,我们随时想替换掉HashMap就变得非常简单了。

第二步:为什么我们不用工厂模式,而要专门做依赖注入框架。
当你只需要一个Map对象的时候很简单。这没错~~~但是你需要创建的对象是一个多层次的对象呢?这样问题就来了。
例如,我要一辆车,那么就需要轮胎,我需要一个轮胎,那么就需要一个轮毂,若干螺丝,外胎,内胎,刹车……
那么问题就来了。我应该做的是造车的时候再种橡胶树么?
这样不对啊。
我们应该做的是,在造车的时候,就发现已经有了生产完全的轮胎,直接调用轮胎装上去就行了。至于轮胎里面需要什么东西,我不是重新做,而是直接调用现成的东西。
这就是依赖注入。
他代表的意义是:我不是先有车才去生产轮胎,而是已经有了轮胎才去生产车。
这样的划分也代表的是一种思维方式,让你的所有对象之间从思维与实现上解耦。这样一来我的对象才是真正的独立。
所以解决你肯定遇到了的一个困惑:XML是一个非常累人的依赖注入实现方式而已,依赖注入其实还有其他实现,例如注解的方式。

第三步,依赖注入是一个好的思想,但是优势在什么地方呢?解耦在什么地方呢?
继续沿用上面的造车的例子。
造车,调用车轮,调用外胎,调用橡胶。
我刚造好一辆车,现在我们需要把车轮换成另外一个车轮。
这样一来,依赖注入的优势就体现出来了,因为你只需要替换掉调用车轮的车轮对象即可。
三步下来,依赖注入的优势就出来了。特别是当团队合作的时候,他完全可以不用理解造车轮的细节就可以完好的换掉车轮对象。

现在你大概了解了什么叫做依赖注入,在spring实战中,作者也强烈推荐用注解方式来操作spring。

简单来说

比如map的实现,你在代码中可能一个类的多个函数、多个类中使用,但是它可能经常会改。于是你决定使用工厂模式。但是你不知道哪些东西可能需要工厂模式去创建,无法提前定义。要是都提前定义的话,那得写多少定义代码,而且遇到嵌套的情况还得代码套代码。
比如构造器简写为 卡车(车轮) 车轮(车胎) 车胎(橡胶) 橡胶(橡胶树) 如果我某天突然想换个车胎 那么就写个新的车胎B 类 车胎B(橡胶) 和车轮(车胎B) 即可 只用改两个地方 1是车胎B的新定义(构造器参数不变) 2是车轮(车胎B) 将依赖换成B 改个构造器类名称就行
而如果我们用new的形式,如果只有一个调用链条还好。我们只会去调用最高层的new 卡车。但是可能卡车有多种,车轮也有多种 此时你更换了车胎B 那么就要依次多个去修改所有的车轮类 去改它们的NEW 车轮。但是如果有了依赖注入 那么你只用在
比如 接口层 服务层 模型层 原来你将微信获取accesstoken的函数(简称g) 放到登录的svc中。这个函数不仅仅被本服务层的调用 可能其他服务、其他接口 也直接调用了这个服务类。于是有一天 你觉得很臃肿 这个函数应该单独搞个manger层,叫微信基础服务层,然后把这个函数放进去,而且奥增加参数,比如?。那么问题来了,你要修改很多地方,登录控制器中不能直接this->g调用了,所以this->g的都要修改。其他的不能再this-》load>登录svc ,然后 this-》登录svc-》g调用了 。后者还好改 可以ide一键替换 但是前面load的呢 可能还用到了原来登录的其他函数把 不能每个都去掉。而如果一开始就依赖注入的思维,那改起来就简单了,???写不下去了

依赖注入是方便单个类 多个函数 或者单个类的一条引用纵向链条 去那啥的 而非方便多个类引入同一个类 然后方便的去修改的
所以 工厂模式和依赖注入可以结合起来

然后 那个修改车胎B的 因为这是链式调用 车胎A其他业务线还要用呢 所以这个不能直接修改车轮 把它换成车胎B 。这个场景并非依赖注入 而应该是工厂模式。 依赖注入是统一的业务去修改的场景

哦 懂了 都不对
依赖注入是将这些东西交给loc自动处理。而工厂模式是手动 特别是当创建某实例有嵌套关系的时候就很麻烦。
上面第三步是依赖注入和工厂共同的好处 而非依赖注入一个的好处 所以我是逻辑搞错了 产生了误解

我自己的

场景1

你写了一个类A,里面很多方法都依赖另一个类B,所谓依赖就理解成需要import 实例化就好。首先,要写很多new的重复代码 很麻烦。其次,那么如果每个函数单独去new,一旦B类修改了名字 那么就有所有函数都改一次。就算你可以ide一键修改 ,但是如果是把某函数换了一个地方呢,移动到了C(可能因为这个分类更合适),因为B还存在,A中用到了B的其他方法 所以不能直接一键替换。而且一键替换也可能出问题把 不能太依赖ide 万一改错了咋整
于是你想到,把这个类,在构造器中创造好呀。

__construt(){
    
this->B = new B();//这里的前面的B 还可以换名字 比如 this-》map = new HashMap
}

但是这样也有两个问题:
1 造成了和B类的强依赖,因为不是所有函数都用到了B类的,而这样造成了,B出问题 所有的A都没办法正常工作
2 B类不光被A使用 如果要改的话 那不是所有的用了B类的都要改?
3 但是如果是把某函数换了一个地方呢,移动到了C(可能因为这个分类更合适),因为B还存在,A中用到了B的其他方法 所以不能直接一键替换。----这个构造器中加一个C就行了,然后一键替换。
4、重复实例化

场景1.1和1.4

此处就可以看php实现那里了 用set的形式,但是这样 参数列表长的时候也很麻烦,于是用容器。公构造方法最终只需要传入一个容器类就行。还能需要的时候再实例化。
这里的别名可以定义在一个单独的文件中 然后依赖B的其他的类也能够直接访问。
但是其实大多数时候 一个类不会一层的去平行依赖这么多其他的类,所以理解的时候,还是用构造器这个例子直接理解就行

场景1.2

工厂模式
比如
collection map=new HashMap();
变为
collection map=MapFactory.createMap();

但是
1、你不知道哪些东西可能需要工厂模式去创建,无法提前定义。要是都提前定义的话,那得写多少定义代码? 遇到嵌套的情况还得代码套代码。
2、而且不是每个类都要改的。

场景1.2.1

总的来说

需要工厂模式统一管理的 就在provider容器中注册别名
不需要的 就构造函数中增加一个类型

其他讲的好的

作者:93号选手
链接:https://www.zhihu.com/question/32108444/answer/121881566
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

假如有一个 船(Chuan)类 ,成员变量中肯定需要一个 桨(Jiang) 类,class Chuan{
Jiang j = new Jiang() ;
} 如果船要干什么事,肯定需要桨的参与。所以是十分 “依赖”桨;来新需求,桨需要统一长度为10米。需要重构:这时候我们需要控制桨的长度为10在构造方法中。我们需要这么写;class Chuan{
Jiang j = new Jiang(10) ;
}
来了,这一个改动同时需要修改桨的类即加入长度属性,又需要修改船其中的new Jiang()代码传入长度(修改两处)。这时候就设计者就思考,为什么我们加入一个特性需要更改两个类中代码(这也就是耦合度高)!所以我们要解耦要依赖注入;常用解耦方式:构造方法注入如下:我重构代码的时候再也不用管依赖的桨长还是短,宽颜色都不管!因为船构造方法依赖了桨。任你桨怎么设计,我用的时候传一个桨进来即可。(下层依赖上层,用的时候传入,而不是针对下层去修改)class Chuan{
Jiang j ;
public Chuan(Jiang j){
this.j = j;
};
}
工厂模式注入新需求如下:桨的生产商觉得不能让你全国各地的船用之前都需要制造一个桨,天津船需要时候天津当地new 一个桨,北京需要 北京当地new一个桨,造成了使用时多处代码依赖桨,这样之后再修改桨的规范就要各处去修改,太麻烦了且容易错误不统一。所以搞一个全国工厂,让桨的生产过程即new的过程就在这一个类中,船就只关注Factory类中方法便可。(核心业务逻辑需要依赖的类,该类实例化交给第三方类来实现注入。)工厂模式 新增个Factory 工厂类去注入; 工厂类如下class Factory {
/**
* 通过msg来确定你要什么长度颜色大小。工厂出一套规范。之后约束拓展在此类就可以进行
**/
public Jiang getJiang(String msg){
if(msg=“10”){
return new Jiang(10)
}else if(msg=“red”){
return new Jiang(“red”)
};
};

}船的代码变为class Chuan {
Jiang j ;
void run(){
Factory h = new Factory();
j=h.getJiang(“red“); //得到了红色的桨
j=h.getJiang(“10“); //得到长度10的桨
};

}框架注入(本质还是工厂设计模式的具体实现)本质也是第三方依赖注入,但是这个第三方可以脱离类。将对象依赖映射信息存储在容器一般为.xml 或者特定的对象中,并实现动态的注入。需要用的时候直接拿就是的。最后本人个人理解:为什么要有依赖注入(一种设计代码模式),因为我们要控制反转(设计代码的思路)。为什么控制反转。因为我们软件设计需要符合软件设计原则依赖倒置(设计代码原则),单一职责原则,开闭原则。归根到底都是,寻找业务中容易变化的点,寻找解耦的点, 让其更加可控,让程序员改代码的时候容易些,同时对系统的影响小。

2

作者:Rootrl
链接:https://www.zhihu.com/question/32108444/answer/417561137
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

举个例子:

class Comment{
    
········其他代码省略
		public function afterInsert()	{
    		
		$notification = new EmailNotification(...);	
			$notification->send(...);
				}}

如上,假如我们在用户提交评论后通知被评论者,这里通知方式是邮件,而且是直接在类中实例化邮件通知类,这样代码耦合度高,如果换个短信通知方式就不得不改这里面代码,具体好的实现我们下面会讲到。#### 依赖注入依赖注入是一种设计模式,是一种IoC的具体实现,实现了IoC自然就符合依赖倒置原则。依赖注入的核心思想是把类中所依赖单元的实例化过程放到类外面中去实现,然后把依赖注入进来。常用的依赖注入方式有属性注入和构造函数注入。比如用构造函数注入解耦上面代码:

// 通知接口
interface Notifaction{
    	
public function send(...);
}
// 短信通知实现通知接口
class SmsNotification implements Notification{
    	
public function send(...)	{
    		
...	}
}

// 评论类
class Comment{
    	
...	
protected $notification;	
public function __construct(Notification $smsNotification)	{
    		
$this->notification = $smsNotification;	
}	
public function afterInsert()	{
    		
$this->notification->send(...);	
}
}

// 实例化短信通知类
$smsNotification = new SmsNotification(...);// 通过构造函数方法注入
$comment = new Comment($smsNotification);
...
$comment->save();

这样,我们先定义Notification接口,里面有个send方法,让后面的通知者不管是邮件类还是短信类都实现这个接口,然后在外面通过构造函数方式注入进来,这样就解决了Comment类对具体通知方法的依赖,只要是实现了Notification接口的,都可以通过构造函数传进来,Comment类完全不用做任何修改。这样无论对于代码维护还是单元测试(可以模拟实现一个Notification类),都非常方便。

3

作者:CHANGEX
链接:https://www.zhihu.com/question/32108444/answer/582118313
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

在Spring中有两个很重要的概念IoC(控制反转)和DI(依赖注入),这两个功能一起使用就能实现解耦。首先我们要明白IoC就是一个工厂模式的实现,DI的作用就是给某个对象设置属性值。最主要的作用是让new对象这件事情做到可配置化,我们在xml里面写的的各种<bean>节点实际上就是在new对象(反射)这么做是如何进行解耦的呢?为什么要这么做呢?首先第一个问题,我们来看一个案例:
按照MVC架构来写代码,我们来看Dao层和Service层:
1.dao层接口TestDao
2.dao层实现类TestDaoImpl
3.service层

不使用IoC和DI:在service需要使用dao层实例:
private TestDao testDao=new TestDaoImpl();
此时,如果我们需要切换dao层实例为TestDaoImpl2该怎么做?
1.编写TestDaoImpl2的相关代码
2.修改service层代码为:private TestDao=new TestDaoImpl2();
这里只有一个service层引用了这个dao,那如果有100个类都引用了这个dao,那么我们需要new100个dao层实现类,还需要在每次修改代码时,找到这100个service去修改其中的代码。

那如果使用IoC和DI呢?
1.在TestDaoImpl上加上@Repository注解
2.在Service层注入:@Autowiredprivate TestDao testDao;
如过需要修改TestDao的实现类,我们需要:
1.编写TestDaoImpl2的具体代码
2.取消TestDaoImpl上的Repository注解
3.在TestDaoImpl2上加上Repository注解
看到没,这一次我们没有修改Service层任何代码,就算有100个类中都注入了TestDao,我们也不需要再去修改了。而且由于是单例模式,这100个service中使用的是同一个dao实例,减少了内存的使用。至于为什么要解耦,实际上如果我们的程序在第一次上线之后如果再也不需要维护和更新的话,完全不需要考虑什么架构啊、解耦啊这些乱七八糟的概念。可现实是,程序上线之后需要不断的维护和更新,难以避免的就需要修改以前的代码,如果不进行解耦,我们在修改某一处代码时可能需要同时去修改很多很多的地方。而解耦就是为了解决这个问题而生的。使用xml进行配置的好处是解决了硬编码的问题,如果我们需要切换实现类,只需要修改xml文件,不需要修改代码,当然也不需要重新编译代码了。

我的解释

这里和上面的不同之处在于 同时用了工厂模式和依赖注入 还用了接口

不用接口 不用工厂 不用依赖注入的方式:
service层 Dao实例类1 变量名= Dao 实例类1
但是 这样在定义service的普通方法的时候 方法的参数类型只能写 Dao实例类1 不方便 要是多个方法都用到了Dao实例类1 要改为实例2的话 要改很多地方

用接口 不用工厂 不用依赖注入的方式:
service层 Dao接口 变量名= Dao 实例类1
这样 至少service层的方法定义 不用都改了,但是如果有100个都用了这个实例类1 现在要换成2 还是就很麻烦

用接口 用工厂 不用依赖注入的方式
service层
Dao接口 变量名=工厂类.createDao实例();
然后就可以统一定义了,但是 这样只能统一去修改 如果我们某些service想用原来的实例类1呢

用接口 用工厂 用依赖注入的方式
1.在Dao实例类1上加@Repository注解
2.在Service层注入:@Autowiredprivate 实例类1接口名
如过需要修改TestDao的实现类,我们需要:
1.编写实例类2的具体代码
2.取消实例类1上的Repository注解
3.在实例类2上加上Repository注解
额 这里出问题了 好像并不能解决上面那个问题啊

ps逻辑拆分法则
事实上 如果我们讲解的时候 方法1也存在后面的问题 但是这样讲不就很乱么 还有交叉 所以像这样 一步一步推动的去讲

4

最开始,我们创建对象直接new出对象,这样的弊端是程序间的依懒性大,不易于维护和扩展,我们引出工厂模式进行解耦,工厂模式的原理是解析(读取)配置文件,通过反射技术来创建对象,这样的好处是以后我们再需要创建对象时,直接向工厂获取需要的对象,降低了耦合性,整个创建对象的过程我们从主动new变成被动工厂为我们创建或查找对象,这种以被动接收方式获取对象的思想就是控制反转, 它也是spring框架的核心之一,但是这里有一个弊端,就是如果循环的调用工厂获取对象,那程序就变成多例模式,也会占用内存空间,为了解决这个问题,我们引出IoC容器的概念,就是工厂在创建每一个对象后我们把对象存在集合中,因为数组长度固定,这里不考虑用数组,List与Map集合的选择在于你有没有查询的需求,我们这里使用的是Map集合存放对象,集合里存放的是反射创建对象的唯一标识和对象,这个Map集合就是IoC容器,通过IoC容器,我们可以获取我们所需要的对象,并建立对象之间的依赖,这个过程称之为依赖注入(DI)

可以把IoC模式看做是工厂模式的升华,可以把IoC看作是一个大工厂,只不过这个大工厂里要生成的对象都是在XML文件中给出定义的,然后利用Java 的“反射”编程,根据XML中给出的类名生成相应的对象。从实现来看,IoC是把以前在工厂方法里写死的对象生成代码,改变为由XML文件来定义,也就是把工厂和对象生成这两者独立分隔开来,目的就是提高灵活性和可维护性。

5

在这里插入图片描述
好像工厂是手动的 注入是自动的 ?

6

在这里插入图片描述

7

在这里插入图片描述

版权声明
本文为[云闲不收]所创,转载请带上原文链接,感谢
https://blog.csdn.net/S_ZaiJiangHu/article/details/124333244