名词解释
官方解释:
IOC - 控制反转 DI - 依赖注入
通俗举例:
小明以前很穷,风餐露宿,居无定所。现在发财了,自己也想拥有属于自己的房子,这个时候小明想,要不回老家盖一栋房子,一来可以住,二来可以光宗耀祖,这个时候,小明需要自己去打造一栋房子;后来小明又想,为何不在城市里直接买套房呢,生活更加丰富多彩也方便。于是,小明就找了房产中介(IOC容器)买了房子(依赖注入),最终小明很快就住上了属于自己的房子,开心快乐极了。。。
小明 依赖 房子,小明从自己盖房子(自己“控制”房子)到找中介买房子(让中介“控制”房子),这就叫做控制反转,也就是IOC;而房产中介根据小明的需求,直接把房子提供给小明(当然小明付钱了),这就叫做依赖注入,也就是DI。
当然,这个房子并不是房产中介建设的,而是开发商建设的,这个开发商就是服务提供者。
三十年后,小明的这套房子格局跟不上时代了,住得不舒服,想改造/重新装修房子,但是时间成本太高了,于是,小明又找房产中介买了房子,小明又很快住上新房子了。。。这也体现了面向对象中类的单一职责原则。
目的
采用IOC思想和DI设计模式,主要目的是:解耦
开车式:异地恋。就算中间隔着一个距离,但也不影响真心的相爱着。
原生代码实现
传统写法
<?php
/**
* Create by PhpStorm
* User : Actor
* Date : 2019-11-01
* Time : 22:03
*/
/**
* Class 购房者
*/
class 购房者
{
private $姓名;
public function __construct($姓名)
{
$this->姓名 = $姓名;
}
public function 买房()
{
$新房 = new 商品房('0001', '四室两厅', '180平方米');
echo '我是'.$this->姓名."\r\n";
echo '我买了'. $新房->获取户型(). '的房子了'."\r\n";
echo '我买了'. $新房->获取面积(). '的房子了'."\r\n";
}
}
/**
* Class 商品房
*/
class 商品房
{
private $房源编号;
private $户型;
private $面积;
public function __construct($房源编号, $户型, $面积)
{
$this->房源编号 = $房源编号;
$this->户型 = $户型;
$this->面积 = $面积;
}
public function 获取面积()
{
return $this->面积;
}
public function 获取户型()
{
return $this->户型;
}
}
$大明 = new 购房者('大明');
$大明->买房();
?>
以上代码输出
[actor20:49:55] /projects/phpAdvanced$ php ioc/iocDriveCar.php
我是大明
我买了四室两厅的房子了
我买了180平方米的房子了
[actor20:56:18] /projects/phpAdvanced$
采用IOC和DI的思想来实现
<?php
/**
* Create by PhpStorm
* User : Actor
* Date : 2019-11-01
* Time : 22:03
*/
/**
* Class 购房者
*/
class 购房者
{
private $姓名;
public function __construct($姓名)
{
$this->姓名 = $姓名;
}
public function 买房(商品房 $新房)
{
echo '我是'.$this->姓名."\r\n";
echo '我买了'. $新房->获取户型(). '的房子了'."\r\n";
echo '我买了'. $新房->获取面积(). '的房子了'."\r\n";
}
}
/**
* Class 商品房
*/
class 商品房
{
private $房源编号;
private $户型;
private $面积;
public function __construct($房源编号, $户型, $面积)
{
$this->房源编号 = $房源编号;
$this->户型 = $户型;
$this->面积 = $面积;
}
public function 获取面积()
{
return $this->面积;
}
public function 获取户型()
{
return $this->户型;
}
}
/**
* 房产中介,就是我们讲的ioc
* Class 房产中介
*/
class 房产中介
{
private $在售房源 = [];//这个类似于laravel的Container对象中的$bindings
private $认筹房源 = [];//类似于laravel的Container对象中的$resolved
private $公租房 = [];//类似于laravel的Container对象中的$instances
private $网红房源 = [];//类似于laravel的Container对象中的$aliases / $abstractAliases
private $意向购房群体 = [];
public function 预售登记($户型, $详细信息)
{
$this->在售房源[$户型] = $详细信息;
}
public function 获取在售房源($户型)
{
return ($this->在售房源[$户型])();//因为是闭包,所以,要增加()来执行闭包函数
}
public function 意向登记($意向人, $个人信息)
{
$this->意向购房群体[$意向人] = $个人信息;
}
public function 获取意向人信息($意向人)
{
return ($this->意向购房群体[$意向人])();//因为是闭包,所以,要增加()来执行闭包函数
}
}
$app = new 房产中介();
$app->预售登记('三室一厅', function(){
return new 商品房('1001', '三室一厅', '100平方米');
});
$app->预售登记('四室两厅', function(){
return new 商品房('1002', '四室两厅', '150平方米');
});
$app->意向登记('小明', function(){
return new 购房者('小明');
});
$app->意向登记('张三', function(){
return new 购房者('张三');
});
//echo $app->获取意向人信息('小明')->买房($app->获取在售房源('四室两厅'));
$意向人 = $app->获取意向人信息('小明');
$新房 = $app->获取在售房源('四室两厅');
$意向人->买房($新房);
?>
以上程序输出
[actor20:49:43] /projects/phpAdvanced$ php ioc/iocDriveCar.php
我是小明
我买了四室两厅的房子了
我买了150平方米的房子了
[actor20:49:55] /projects/phpAdvanced$
对IOC和DI的本质分析
从上面的代码,我们看到,房产中介作为IOC,其实本质就是数组(可以是一维数组,也可以是多维数组)。
其实,在laravel框架中,Container
对象中的属性$bindings
、$resolved
、$instances
、$aliases
、$abstractAliases
其实也是从不同维度来管理注册到容器中的对象的。
上面的例子中,如果从业务逻辑角度来讲,无非就是购房者要买房,主要的类有:购房者、商品房。
如果按照传统代码来实现,那么购房者对象对商品房对象的依赖是强依赖,因为在购房者类中需要new 商品房()
而在采用IOC和DI的思想来实现的话,增加了房产中介对象这个IOC容器,商品房首先在房产中介那边进行一下预售登记,购房者也在房产中介那边进行一下意向登记,购房者对象需要依赖商品房对象,采用依赖注入,即:让房产中介直接把实例化后到商品房对象注入到购房者对象,购房者对象无需关注怎么实例化,只管拿过来用就行。
Laravel框架IOC核心源码——绑定
我们来简单看下Laravel框架的核心容器的绑定是怎么实现的?
以下代码都是在Illuminate\Container\Container类中
- 通过instance方法绑定
/**
* Register an existing instance as shared in the container.
*
* @param string $abstract
* @param mixed $instance
* @return mixed
*/
public function instance($abstract, $instance)
{
$this->removeAbstractAlias($abstract);
$isBound = $this->bound($abstract);
unset($this->aliases[$abstract]);
// We'll check to determine if this type has been bound before, and if it has
// we will fire the rebound callbacks registered with the container and it
// can be updated with consuming classes that have gotten resolved here.
$this->instances[$abstract] = $instance;
if ($isBound) {
$this->rebound($abstract);
}
return $instance;
}
使用该方法注册绑定到容器中的对象实例是共享的。
- bind方法
/**
* Register a binding with the container.
*
* @param string $abstract
* @param \Closure|string|null $concrete
* @param bool $shared
* @return void
*/
public function bind($abstract, $concrete = null, $shared = false)
{
$this->dropStaleInstances($abstract);
// If no concrete type was given, we will simply set the concrete type to the
// abstract type. After that, the concrete type to be registered as shared
// without being forced to state their classes in both of the parameters.
if (is_null($concrete)) {
$concrete = $abstract;
}
// If the factory is not a Closure, it means it is just a class name which is
// bound into this container to the abstract type and we will just wrap it
// up inside its own Closure to give us more convenience when extending.
if (! $concrete instanceof Closure) {
$concrete = $this->getClosure($abstract, $concrete);
}
$this->bindings[$abstract] = compact('concrete', 'shared');
// If the abstract type was already resolved in this container we'll fire the
// rebound listener so that any objects which have already gotten resolved
// can have their copy of the object updated via the listener callbacks.
if ($this->resolved($abstract)) {
$this->rebound($abstract);
}
}
如何使用bind方法来将对象注册绑定到容器中呢?如下,bind方法是将闭包绑定到容器当中
$this->app->bind('User\API', function ($app) {
return new User\API($app->make('UserLogin'));
});
同样,bind方法会先删除旧的实例,然后再新的实例放入闭包中,再绑定到容器中。如果第二个参数不是闭包,会通过getClosure方法将类名封装到闭包中,然后在闭包中通过make方法或build方法解析绑定的类。绑定时会将闭包和是否是shared放入$this->bind[]数组中,解析时调用。
- singleton方法
/**
* Register a shared binding in the container.
*
* @param string $abstract
* @param \Closure|string|null $concrete
* @return void
*/
public function singleton($abstract, $concrete = null)
{
$this->bind($abstract, $concrete, true);
}
从官方的代码可以看出,singleton方法,最终还是调用了$this->bind()方法,只是通过singleton()方法绑定到容器的对象只会被解析一次,之后的调用都返回相同的实例,这就是所谓的单例。
Laravel框架IOC核心源码——解析
看源码
/**
* Resolve the given type from the container.
*
* @param string $abstract
* @param array $parameters
* @return mixed
*
* @throws \Illuminate\Contracts\Container\BindingResolutionException
*/
public function make($abstract, array $parameters = [])
{
return $this->resolve($abstract, $parameters);
}
/**
* Resolve the given type from the container.
*
* @param string $abstract
* @param array $parameters
* @param bool $raiseEvents
* @return mixed
*
* @throws \Illuminate\Contracts\Container\BindingResolutionException
*/
protected function resolve($abstract, $parameters = [], $raiseEvents = true)
{
$abstract = $this->getAlias($abstract);
$needsContextualBuild = ! empty($parameters) || ! is_null(
$this->getContextualConcrete($abstract)
);
// If an instance of the type is currently being managed as a singleton we'll
// just return an existing instance instead of instantiating new instances
// so the developer can keep using the same objects instance every time.
if (isset($this->instances[$abstract]) && ! $needsContextualBuild) {
return $this->instances[$abstract];
}
$this->with[] = $parameters;
$concrete = $this->getConcrete($abstract);
// We're ready to instantiate an instance of the concrete type registered for
// the binding. This will instantiate the types, as well as resolve any of
// its "nested" dependencies recursively until all have gotten resolved.
if ($this->isBuildable($concrete, $abstract)) {
$object = $this->build($concrete);
} else {
$object = $this->make($concrete);
}
// If we defined any extenders for this type, we'll need to spin through them
// and apply them to the object being built. This allows for the extension
// of services, such as changing configuration or decorating the object.
foreach ($this->getExtenders($abstract) as $extender) {
$object = $extender($object, $this);
}
// If the requested type is registered as a singleton we'll want to cache off
// the instances in "memory" so we can return it later without creating an
// entirely new instance of an object on each subsequent request for it.
if ($this->isShared($abstract) && ! $needsContextualBuild) {
$this->instances[$abstract] = $object;
}
if ($raiseEvents) {
$this->fireResolvingCallbacks($abstract, $object);
}
// Before returning, we will also set the resolved flag to "true" and pop off
// the parameter overrides for this build. After those two things are done
// we will be ready to return back the fully constructed class instance.
$this->resolved[$abstract] = true;
array_pop($this->with);
return $object;
}
/**
* Instantiate a concrete instance of the given type.
*
* @param string $concrete
* @return mixed
*
* @throws \Illuminate\Contracts\Container\BindingResolutionException
*/
public function build($concrete)
{
// If the concrete type is actually a Closure, we will just execute it and
// hand back the results of the functions, which allows functions to be
// used as resolvers for more fine-tuned resolution of these objects.
if ($concrete instanceof Closure) {
return $concrete($this, $this->getLastParameterOverride());
}
try {
$reflector = new ReflectionClass($concrete);
} catch (ReflectionException $e) {
throw new BindingResolutionException("Target class [$concrete] does not exist.", 0, $e);
}
// If the type is not instantiable, the developer is attempting to resolve
// an abstract type such as an Interface or Abstract Class and there is
// no binding registered for the abstractions so we need to bail out.
if (! $reflector->isInstantiable()) {
return $this->notInstantiable($concrete);
}
$this->buildStack[] = $concrete;
$constructor = $reflector->getConstructor();
// If there are no constructors, that means there are no dependencies then
// we can just resolve the instances of the objects right away, without
// resolving any other types or dependencies out of these containers.
if (is_null($constructor)) {
array_pop($this->buildStack);
return new $concrete;
}
$dependencies = $constructor->getParameters();
// Once we have all the constructor's parameters we can create each of the
// dependency instances and then use the reflection instances to make a
// new instance of this class, injecting the created dependencies in.
try {
$instances = $this->resolveDependencies($dependencies);
} catch (BindingResolutionException $e) {
array_pop($this->buildStack);
throw $e;
}
array_pop($this->buildStack);
return $reflector->newInstanceArgs($instances);
}
从上面的第三段代码build()方法中可以看出,解析时,如果绑定注册的是闭包函数,那么就是直接返回闭包函数的执行,关键代码如下
if ($concrete instanceof Closure) {
return $concrete($this, $this->getLastParameterOverride());
}
如果绑定注册的是类名,那么就利用php的反射(ReflectionClass)来实例化对象,并返回给调用者。bind()方法剩下的代码干的就是这个工作。
最后,在resolve()方法中的
$this->resolved[$abstract] = true;
代表已经解析成功了。