基本概念
1.依赖倒置(反转)原则(DIP):一种软件架构设计的原则(抽象概念,是一种思想)
在面向对象编程领域中,依赖反转原则(Dependency inversion principle,DIP)是指一种特定的解耦(传统的依赖关系创建在高层次上,而具体的策略设置则应用在低层次的模块上)形式,使得高层次的模块不依赖于低层次的模块的实现细节,依赖关系被颠倒(反转),从而使得低层次模块依赖于高层次模块的需求抽象。
该原则规定:
- 1.高层次的模块不应该依赖于低层次的模块,两者都应该依赖于抽象接口。
- 2.抽象接口不应该依赖于具体实现。而具体实现则应该依赖于抽象接口。
在上图中,高层对象A依赖于底层对象B的实现;图2中,把高层对象A对底层对象的需求抽象为一个接口A,底层对象B实现了接口A,这就是依赖反转。
该原则颠倒了一部分人对于面向对象设计的认识方式。如高层次和低层次对象都应该依赖于相同的抽象接口。它转换了依赖,高层模块不依赖于低层模块的实现,而低层模块依赖于高层模块定义的接口。通俗的讲,就是高层模块定义接口,低层模块负责实现。
2.控制反转(IoC):一种反转流、依赖和接口的方式(DIP的具体实现方式,一种设计原则)
控制反转(Inversion of Control,缩写为IoC),是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。其中最常见的方式叫做依赖注入(Dependency Injection,简称DI),还有一种方式叫“依赖查找”(Dependency Lookup)。通过控制反转,对象在被创建的时候,由一个调控系统内所有对象的外界实体,将其所依赖的对象的引用传递给它。也可以说,依赖被注入到对象中。
它把传统上由程序代码直接操控的对象的调用权交给容器,通过容器来实现对象组件的装配和管理。所谓的“控制反转”概念就是对组件对象控制权的转移,从程序代码本身转移到了外部容器。
实现控制反转主要有两种方式:
1.依赖注入:
2.依赖查找
两者的区别在于,前者是被动的接收对象,在类A的实例创建过程中即创建了依赖的B对象,通过类型或名称来判断将不同的对象注入到不同的属性中,而后者是主动索取相应类型的对象,获得依赖对象的时间也可以在代码中自由控制。
3.依赖注入(DI):IoC的一种实现方式,用来反转依赖(IoC的具体实现方式)
依赖注入有如下实现方式:
- 接口注入(Interface Injection):实现特定接口以供外部容器注入所依赖类型的对象。
- 设值注入(Setter Injection): 实现特定属性的public set方法,来让外部容器调用传入所依赖类型的对象。
- 构造器注入(Constructor Injection): 实现特定参数的构造函数,在新建对象时传入所依赖类型的对象。
- 基于注解 : 基于Java的注解功能,在私有变量前加“@Autowired”等注解,不需要显式的定义以上三种代码,便可以让外部容器传入对应的对象。该方案相当于定义了public的set方法,但是因为没有真正的set方法,从而不会为了实现依赖注入导致暴露了不该暴露的接口(因为set方法只想让容器访问来注入而并不希望其他依赖此类的对象访问)。
3.依赖查找(DL):IoC的一种实现方式,用来反转依赖(IoC的具体实现方式)
依赖查找更加主动,在需要的时候通过调用框架提供的方法来获取对象,获取时需要提供相关的配置文件路径、key等信息来确定获取对象的状态
小结
依赖倒置原则(DIP):一种软件架构设计的原则(抽象概念,一种思想)。
控制反转(IoC):一种反转流、依赖和接口的方式(DIP的具体实现方式)。
依赖注入(DI):IoC的一种实现方式,用来反转依赖(IoC的具体实现方式)。
IoC容器(也称DI Container):提供了动态地(自动化)创建、注入依赖单元,映射依赖关系等功能,减少了许多代码量(DI框架)。
需要注意的一些地方
1.控制反转的层面
在传统的应用中,程序流程的顺序是由开发者主导的,由于IoC,主导权转移到了框架的手里(因为IoC容器)
2.控制反转需要解决的问题
查找,生成所需的实例,返回给需要者(因此又叫依赖注入)
3.实现依赖注入的目的
尽管一个类A对它所依赖的类B是如何实现的一无所知,类A依然能够与类B通信(通过定义一些通用接口)。类B在开发中可能会有多种实现,依赖注入(同时也是IoC)解决的问题就是自动地将这些类B的实现在需要的时候传递给类A。
4.如何实现依赖注入
最基本的思路是构造一个独立的类,它的功能就是统一为其他所有类的依赖生成所需的实例(assembler,类似容器),然后构造并返回这个类
依赖注入的具体实现方式(主要介绍设值注入和构造器注入)
备注:对类A,类B,类C的定义如下
类A (需要通过容器获取的)
类B (类A的依赖,广义上的接口)
类C (类B的具体实现)
1.构造器注入
- a)在类A的构造器参数列表中定义了该类所有需要被依赖注入的东西(类B)
- b)在容器中需要先定义好某个接口(广义上的interface,即类B)关联的某个具体实现类(有时还需要配置一些具体参数,即类C),这些容器配置在不同的开发中很可能是不一样的。通常这些配置会是一个独立的文件
- c)在需要某个类A的时候通过容器来生成而不是直接new
class MovieLister... (MovieLister相当于类A,MovieFinder相当于类B)
public MovieLister(MovieFinder finder) {
this.finder = finder;
}
class ColonMovieFinder... (这个相当于类c)
public ColonMovieFinder(String filename) {
this.filename = filename;
}
(这里返回的pico就是IoC容器)
private MutablePicoContainer configureContainer() {
MutablePicoContainer pico = new DefaultPicoContainer();
Parameter[] finderParams = {new ConstantParameter("movies1.txt")};
//在使用容器前需要先配置,下面的代码就是对容器的配置
pico.registerComponentImplementation(MovieFinder.class, ColonMovieFinder.class, finderParams);
pico.registerComponentImplementation(MovieLister.class);
return pico;
}
下面是通过容器来获得类A的过程
public void testWithPico() {
MutablePicoContainer pico = configureContainer();//获得一个配置好的容器
MovieLister lister = (MovieLister) pico.getComponentInstance(MovieLister.class);//通过容器来获得类A
Movie[] movies = lister.moviesDirectedBy("Sergio Leone");
assertEquals("Once Upon a Time in the West", movies[0].getTitle());
}
2.setter注入
- a)在类A中为所有需要注入的依赖类(类B)创建setter方法
- b)在独立的文件配置类A中的依赖的具体实现(即配置类B的具体实现类C)
- c)通过容器生成生成类A
class MovieLister... (同样的MovieLister为类A,MovieFinder为类B)
private MovieFinder finder;
public void setFinder(MovieFinder finder) {
this.finder = finder;
}
class ColonMovieFinder... (这个同样的相当于类C)
public void setFilename(String filename) {
this.filename = filename;
}
//下面是容器的配置
<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>
public void testWithSpring() throws Exception {
ApplicationContext ctx = new FileSystemXmlApplicationContext("spring.xml");//获得配置好的容器
MovieLister lister = (MovieLister) ctx.getBean("MovieLister"); //通过容器获取类A
Movie[] movies = lister.moviesDirectedBy("Sergio Leone");
assertEquals("Once Upon a Time in the West", movies[0].getTitle());
}
Service Locator
与DI类似,Service Locator也是用来打破依赖的
基本思想
提供一个独立的类(即Service Locator),它能够为整个应用提供所需的所有service(也可以理解为component)。
具体实现
1.在类A中,依赖的所有类都是通过Service Locator获取的
MovieFinder finder = ServiceLocator.movieFinder();
2.通过配置可以定制在Service Locator中实现如何返回一个特定实例,这个与DI类似
小结
1.实际上可以将Service Locator和DI结合使用,在类A中通过Service Locator获取依赖,而在Service Locator中则可以通过DI来实现获取具体的实例(或者将Service Locator与DI互换也可以?)
2.动态的Service Locator:使用一张映射表,通过查表实现(或直接获取)具体的实例
3.Service Locator与DI 的区别:使用Service Locator时是显式地调用Locator,而Di并没有显式地调用
Yii2中的依赖注入
相关的类:
- yii\di\Container 容器
- yii\di\instance 容器或Service Locator中的东西: 本质上是对于某一个类实例的引用
- yii\di\ServiceLocator
1.yii\di\instance
主要用在两个地方:
- 1.在配置DI容器的时候,使用Instance来引用一个类名,接口名或者是别名(即Instance的id属性)。因此后续DI容器可以将这个引用解析成相应的对象
- 2.用在那些使用service locator获取依赖对象的类中
对于 yii\di\Instance:
1.表示的是容器中的内容,代表的是对于实际对象的引用。
2.DI容器可以通过他获取所引用的实际对象。
3.Instance类仅有的一个属性id一般表示的是实例的类型(即component ID, class name, interface name or alias name)。
2.yii\di\Container
注意:下面所说的“对象类型”的具体定义为“类名,接口名,别名”
对于yiidiContainer
- a) 5个私有属性(都是数组):$_singletons,$_definitions,$_params,$_reflections,$_dependencies
- b) $_singletons // 用于保存单例Singleton对象,以对象类型为键
- c) $_definitions // 用于保存依赖的定义,以对象类型为键
- d) $_params // 用于保存构造函数的参数,以对象类型为键
- e) $_reflections // 用于缓存ReflectionClass对象,以对象类型为键
- f) $_dependencies // 用于缓存依赖信息,以对象类型为键
注意
1.在DI容器中,依赖关系的定义是唯一的。 后定义的同名依赖,会覆盖前面定义好的依赖。
2.上面的键具体就是:带命名空间的类名,接口名,或者是一个别名
3.对于 $_definitions 数组中的元素,它要么是一个包含了”class” 元素的数组,要么是一个PHP callable, 再要么就是一个具体对象。这就是规范化后的最终结果
4.对于$_singletons数组中的元素,要不就是null(表示还未实例化),要不就是一个具体的实例
5.对于$_params数组中的元素,就是一个数组,包含构造函数的所有参数
6.对于$_reflections数组中的元素,就是一个ReflectionClass对象
7.setter注入可以在实例化后
yiidiContainer使用的具体过程
一个简单的例子
namespace app\models;
use yii\base\Object;
use yii\db\Connection;
use yii\di\Container;
interface UserFinderInterface
{
function findUser();
}
class UserFinder extends Object implements UserFinderInterface
{
public $db;
public function __construct(Connection $db, $config = [])
{
$this->db = $db;
parent::__construct($config);
}
public function findUser()
{
}
}
class UserLister extends Object
{
public $finder;
public function __construct(UserFinderInterface $finder, $config = [])
{
$this->finder = $finder;
parent::__construct($config);
}
}
$container = new Container;
$container->set('yii\db\Connection', [
'dsn' => '...',
]);
$container->set('app\models\UserFinderInterface', [
'class' => 'app\models\UserFinder',
]);
$container->set('userLister', 'app\models\UserLister');
$lister = $container->get('userLister');
// which is equivalent to:
$db = new \yii\db\Connection(['dsn' => '...']);
$finder = new UserFinder($db);
$lister = new UserLister($finder);
1.在类A的构造器参数列表中定义了该类所有需要被依赖注入的东西(类B)
2.注册依赖:
a)yii\di\Container::set()
b)yii\di\Container::setSinglton()
使用到了$_definitions ,$_params, $_singletons
3.对象的实例化
a)解析依赖信息
yii\di\Container::getDependencies() (会被后续的build()调用)
getDependencies():操作$_reflections与$_dependencies
1.会向$_reflections 和 $_dependencies写入信息
2.使用PHP的反射机制来获取类的有关信息,主要就是为了从构造器中获取依赖信息,会将反射得到的信息写入$_reflections
3.将从构造器中获取的依赖信息(即构造函数的参数列表)写入$_dependencies
4.返回值: 数组[$reflection, $dependencies]
yii\di\Container::resolveDependencies() (同样的会被后续的build()调用)
resolveDependencies()利用getDependencies()获得的信息进一步具体处理(递归调用)。处理依赖信息, 将依赖信息中保存的Instance实例所引用的类或接口进行实例化。
b)创建实例
yii\di\Container::build()
由getDependencies()获得第一层依赖
由resolveDependencies()递归分析依赖,最终生成所有依赖的实例
$reflection->newInstanceArgs($dependencies);//生成所有依赖后生成这个实例
注意:DI容器只支持 yiibaseObject 类,也就是说如果你想你的类可以放在DI容器里,那么必须继承自 yiibaseObject 类。
4.获取依赖实例化对象
yii\di\Container::get()
a)如果是已经实例化的单例,直接返回($_singletons)
b)如果是尚未定义(不存在于$_definition),则说明其实例化没有依赖,调用build()
c)存在$_definition
i.$definition为callable,直接调用
ii.$definition为数组,根据$definition数组中的‘class’,递归调用get(),递归终止的条件是(当具体实现类就是当前的依赖类时),递归结束时调用build()进行实例化
iii.$definition为对象,直接返回该对象,并将该对象设置为单例
setSinglton()类似
public function set($class, $definition = [], array $params = [])
{
//normalizeDefinition()处理后,返回值要么是一个包含了”class” 元素的数组,要么是一个PHP callable, 再要么就是一个具体对象
$this->_definitions[$class] = $this->normalizeDefinition($class, $definition);
$this->_params[$class] = $params;
unset($this->_singletons[$class]);
return $this;
}
public function get($class, $params = [], $config = [])
{
if (isset($this->_singletons[$class])) {//是单例,且已经实例化(不为null)
// singleton
return $this->_singletons[$class];
} elseif (!isset($this->_definitions[$class])) {//还没有定义过,需要build
return $this->build($class, $params, $config);
}
$definition = $this->_definitions[$class];
if (is_callable($definition, true)) {
$params = $this->resolveDependencies($this->mergeParams($class, $params));
$object = call_user_func($definition, $this, $params, $config);
} elseif (is_array($definition)) {
$concrete = $definition['class'];
unset($definition['class']);
$config = array_merge($definition, $config);
$params = $this->mergeParams($class, $params);
if ($concrete === $class) {//$concrete相当于之前提到的具体实现类C,而$class则相当于接口类B
$object = $this->build($class, $params, $config);
} else {
$object = $this->get($concrete, $params, $config);//递归,直到找到具体的实现类C
}
} elseif (is_object($definition)) {
return $this->_singletons[$class] = $definition;
} else {
throw new InvalidConfigException('Unexpected object definition type: ' . gettype($definition));
}
if (array_key_exists($class, $this->_singletons)) {
// singleton
$this->_singletons[$class] = $object;
}
return $object;
}
protected function build($class, $params, $config)
{
/* @var $reflection ReflectionClass */
list ($reflection, $dependencies) = $this->getDependencies($class); //获取第一层依赖关系
foreach ($params as $index => $param) {
$dependencies[$index] = $param; //额外提供的构造函数参数,添加到依赖中
}
$dependencies = $this->resolveDependencies($dependencies, $reflection);//递归解析依赖,并会在此返回依赖的实例
if (!$reflection->isInstantiable()) {
throw new NotInstantiableException($reflection->name);
}
if (empty($config)) {
return $reflection->newInstanceArgs($dependencies);//通过反射实例生成对象
}
//config中的对象作为该类的property使用
if (!empty($dependencies) && $reflection->implementsInterface('yii\base\Configurable')) {
// set $config as the last parameter (existing one will be overwritten)
$dependencies[count($dependencies) - 1] = $config;
return $reflection->newInstanceArgs($dependencies);
} else {
$object = $reflection->newInstanceArgs($dependencies);
foreach ($config as $name => $value) {
$object->$name = $value;
}
return $object;
}
}
protected function getDependencies($class)
{
if (isset($this->_reflections[$class])) {//如果已经反射解析过则直接返回
return [$this->_reflections[$class], $this->_dependencies[$class]];
}
$dependencies = [];
$reflection = new ReflectionClass($class);
//构造函数的参数即这个类的依赖
$constructor = $reflection->getConstructor();
if ($constructor !== null) {
foreach ($constructor->getParameters() as $param) {
if ($param->isDefaultValueAvailable()) {
$dependencies[] = $param->getDefaultValue();
} else {
$c = $param->getClass();//这里要能获取到类名需要在构造函数中用类型限制参数,否则获取到null,而且注意对于php的基本类型,获取到的也是null
$dependencies[] = Instance::of($c === null ? null : $c->getName());
}
}
}
$this->_reflections[$class] = $reflection;
$this->_dependencies[$class] = $dependencies;
return [$reflection, $dependencies];
}
protected function resolveDependencies($dependencies, $reflection = null)
{
foreach ($dependencies as $index => $dependency) {
if ($dependency instanceof Instance) {
if ($dependency->id !== null) { //这里的dependency是Instance的实例
$dependencies[$index] = $this->get($dependency->id);
} elseif ($reflection !== null) {
$name = $reflection->getConstructor()->getParameters()[$index]->getName();
$class = $reflection->getName();
throw new InvalidConfigException("Missing required parameter \"$name\" when instantiating \"$class\".");
}
}
}
return $dependencies;
}
递归调用的示意图
在Yii2中调用组件
先看一下各个类的继承关系
下面以Yii::$app->db为例
1.配置组件
配置的内容:
'components' => [
'db' => [
'class' => 'yii\db\Connection',
'dsn' => 'mysql:host=localhost;dbname=wechat',
'username' => 'root',
'password' => 'michael',
'charset' => 'utf8',
],
2.在框架的启动过程中加载组件的定义
Yii2的启动
入口脚本:
(new yii\web\Application($config))->run();
1.new yii\web\Application($config)
2.run()
yii\web\Application的构造函数
public function __construct($config = [])
{
Yii::$app = $this;
static::setInstance($this); //将当前module存到Yii::$app->loadedModules[]
$this->state = self::STATE_BEGIN;
//1.通过$config配置别名,基本参数
//2.配置核心组件(仅仅是配置,将Yii框架写好的配置与自己的配置合并)
$this->preInit($config);
$this->registerErrorHandler($config);
//下面这一行是重点,Component是当前类的祖先
//在下面的构造函数中执行了Yii::configure($this, $config),将$config中的配置作为属性添加到$app中
Component::__construct($config);
}
Component::__construct($config)
//实际上下面这个构造函数的定义在yii\base\Object中
public function __construct($config = [])
{
if (!empty($config)) {
Yii::configure($this, $config);
}
$this->init();
}
Yii::configure($this, $config)
public static function configure($object, $properties)
{
foreach ($properties as $name => $value) {
//下面这行代码会触发魔术方法 ($object->components = $value)
//实际执行的代码是ServiceLocator::setComponents($components)
$object->$name = $value;
}
return $object;
}
ServiceLocator::setComponents($components)
public function setComponents($components)
{
foreach ($components as $id => $component) {
$this->set($id, $component);
}
}
最终加载组件配置的代码
//从下面的代码中可以看到,最终组件的配置被存储在$app->_definitions数组中
public function set($id, $definition)
{
if ($definition === null) {
unset($this->_components[$id], $this->_definitions[$id]);
return;
}
unset($this->_components[$id]);
if (is_object($definition) || is_callable($definition, true)) {
// an object, a class name, or a PHP callable
$this->_definitions[$id] = $definition;
} elseif (is_array($definition)) {
// a configuration array
if (isset($definition['class'])) {
$this->_definitions[$id] = $definition;
} else {
throw new InvalidConfigException("The configuration for the \"$id\" component must contain a \"class\" element.");
}
} else {
throw new InvalidConfigException("Unexpected configuration type for the \"$id\" component: " . gettype($definition));
}
}
3.获取组件
Yii::$app->db会触发魔术方法,调用ServiceLocator::__get()
public function __get($name)
{
if ($this->has($name)) {
//已经定义过组件
return $this->get($name);
} else {
//没有定义过组件
return parent::__get($name);
}
}
public function has($id, $checkInstance = false)
{
//这里因为在框架启动过程中将组件的配置加载到$_definitions中了,所以会返回true
return $checkInstance ? isset($this->_components[$id]) : isset($this->_definitions[$id]);
}
//通过下面的代码可以看到,如果组件已经实例化过存储在$_components中了,就直接返回
//否则通过Yii::createObject($definition)来生成组件实例,并存储到$_components中
public function get($id, $throwException = true)
{
if (isset($this->_components[$id])) {
return $this->_components[$id];
}
if (isset($this->_definitions[$id])) {
$definition = $this->_definitions[$id];
if (is_object($definition) && !$definition instanceof Closure) {
return $this->_components[$id] = $definition;
} else {
return $this->_components[$id] = Yii::createObject($definition);
}
} elseif ($throwException) {
throw new InvalidConfigException("Unknown component ID: $id");
} else {
return null;
}
}
Yii::createObject($definition)
//在Yii2框架中要使用DI来生成对象的话,可以通过调用Yii::createObject($definition)实现
public static function createObject($type, array $params = [])
{
if (is_string($type)) {
//使用容器
return static::$container->get($type, $params);
} elseif (is_array($type) && isset($type['class'])) {
$class = $type['class'];
unset($type['class']);
return static::$container->get($class, $params, $type);
} elseif (is_callable($type, true)) {
return static::$container->invoke($type, $params);
} elseif (is_array($type)) {
throw new InvalidConfigException('Object configuration must be an array containing a "class" element.');
}
throw new InvalidConfigException('Unsupported configuration type: ' . gettype($type));
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。