服务定位器(Service Locator)

跟DI容器类似,引入Service Locator目的也在于解耦。有许多成熟的设计模式也可用于解耦,但在Web应用上, Service Locator绝对占有一席之地。 对于Web开发而言,Service Locator天然地适合使用, 主要就是因为Service Locator模式非常贴合Web这种基于服务和组件的应用的运作特点。 这一模式的优点有:

  • Service Locator充当了一个运行时的链接器的角色,可以在运行时动态地修改一个类所要选用的服务, 而不必对类作任何的修改。
  • 一个类可以在运行时,有针对性地增减、替换所要用到的服务,从而得到一定程度的优化。
  • 实现服务提供方、服务使用方完全的解耦,便于独立测试和代码跨框架复用。

Service Locator的基本功能

在Yii中Service Locator由 yii\di\ServiceLocator 来实现。 从代码组织上,Yii将Service Locator放到与DI同一层次来对待,都组织在 yii\di 命名空间下。 下面是Service Locator的源代码:

 
class ServiceLocator extends Component
{
// 用于缓存服务、组件等的实例
private $_components = []; // 用于保存服务和组件的定义,通常为配置数组,可以用来创建具体的实例
private $_definitions = []; // 重载了 getter 方法,使得访问服务和组件就跟访问类的属性一样。
// 同时,也保留了原来Component的 getter所具有的功能。
// 请留意,ServiceLocator 并未重载 __set(),
// 仍然使用 yii\base\Component::__set()
public function __get($name)
{
... ...
} // 对比Component,增加了对是否具有某个服务和组件的判断。
public function __isset($name)
{
... ...
} // 当 $checkInstance === false 时,用于判断是否已经定义了某个服务或组件
// 当 $checkInstance === true 时,用于判断是否已经有了某人服务或组件的实例
public function has($id, $checkInstance = false)
{
return $checkInstance ? isset($this->_components[$id]) :
isset($this->_definitions[$id]);
} // 根据 $id 获取对应的服务或组件的实例
public function get($id, $throwException = true)
{
... ...
} // 用于注册一个组件或服务,其中 $id 用于标识服务或组件。
// $definition 可以是一个类名,一个配置数组,一个PHP callable,或者一个对象
public function set($id, $definition)
{
... ...
} // 删除一个服务或组件
public function clear($id)
{
unset($this->_definitions[$id], $this->_components[$id]);
} // 用于返回Service Locator的 $_components 数组或 $_definitions 数组,
// 同时也是 components 属性的getter函数
public function getComponents($returnDefinitions = true)
{
... ...
} // 批量方式注册服务或组件,同时也是 components 属性的setter函数
public function setComponents($components)
{
... ...
}
}
 

从代码可以看出,Service Locator继承自 yii\base\Component ,这是Yii中的一个基础类, 提供了属性、事件、行为等基本功能,关于Component的有关知识,可以看看 属性(Property) 、 事件(Event) 和 行为(Behavior) 。

Service Locator 通过 __get() __isset() has() 等方法, 扩展了 yii\base\Component 的最基本功能,提供了对于服务和组件的属性化支持。

从功能来看,Service Locator提供了注册服务和组件的 set() setComponents() 等方法, 用于删除的 clear() 。用于读取的 get() 和 getComponents()等方法。

细心的读者可能一看到 setComponents() 和 getComponents() 就猜到了, Service Locator还具有一个可读写的 components 属性。

Service Locator的数据结构

从上面的代码中,可以看到Service Locator维护了两个数组, $_components 和 $_definitions 。这两个数组均是以服务或组件的ID为键的数组。

其中, $_components 用于缓存存Service Locator中的组件或服务的实例。 Service Locator 为其提供了getter和setter。使其成为一个可读写的属性。 $_definitions 用于保存这些组件或服务的定义。这个定义可以是:

  • 配置数组。在向Service Locator索要服务或组件时,这个数组会被用于创建服务或组件的实例。 与DI容器的要求类似,当定义是配置数组时,要求配置数组必须要有 class 元素,表示要创建的是什么类。不然你让Yii调用哪个构造函数?
  • PHP callable。每当向Service Locator索要实例时,这个PHP callable都会被调用,其返回值,就是所要的对象。 对于这个PHP callable有一定的形式要求,一是它要返回一个服务或组件的实例。 二是它不接受任何的参数。 至于具体原因,后面会讲到。
  • 对象。这个更直接,每当你索要某个特定实例时,直接把这个对象给你就是了。
  • 类名。即,使得 is_callable($definition, true) 为真的定义。

从 yii\di\ServiceLocator::set() 的代码:

public function set($id, $definition)
{
// 当定义为 null 时,表示要从Service Locator中删除一个服务或组件
if ($definition === null) {
unset($this->_components[$id], $this->_definitions[$id]);
return;
} // 确保服务或组件ID的唯一性
unset($this->_components[$id]); // 定义如果是个对象或PHP callable,或类名,直接作为定义保存
// 留意这里 is_callable的第二个参数为true,所以,类名也可以。
if (is_object($definition) || is_callable($definition, true)) {
// 定义的过程,只是写入了 $_definitions 数组
$this->_definitions[$id] = $definition; // 定义如果是个数组,要确保数组中具有 class 元素
} elseif (is_array($definition)) {
if (isset($definition['class'])) {
// 定义的过程,只是写入了 $_definitions 数组
$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));
}
}

  

服务或组件的ID在Service Locator中是唯一的,用于区别彼此。在任何情况下,Service Locator中同一ID只有一个实例、一个定义。也就是说,Service Locator中,所有的服务和组件,只保存一个单例。 这也是正常的逻辑,既然称为服务定位器,你只要给定一个ID,它必然返回一个确定的实例。这一点跟DI容器是一样的。

Service Locator 中ID仅起标识作用,可以是任意字符串,但通常用服务或组件名称来表示。 如,以 db 来表示数据库连接,以 cache 来表示缓存组件等。

至于批量注册的 yii\di\ServiceLocator::setCompoents() 只不过是简单地遍历数组,循环调用 set() 而已。 就算我不把代码贴出来,像你这么聪明的,一下子就可以自己写出来了。

向Service Locator注册服务或组件,其实就是向 $_definitions 数组写入信息而已。

访问Service Locator中的服务

Service Locator重载了 __get() 使得可以像访问类的属性一样访问已经实例化好的服务和组件。 下面是重载的 __get() 方法:

public function __get($name)
{
// has() 方法就是判断 $_definitions 数组中是否已经保存了服务或组件的定义
// 请留意,这个时候服务或组件仅是完成定义,不一定已经实例化
if ($this->has($name)) { // get() 方法用于返回服务或组件的实例
return $this->get($name); // 未定义的服务或组件,那么视为正常的属性、行为,
// 调用 yii\base\Component::__get()
} else {
return parent::__get($name);
}
}

  

在注册好了服务或组件定义之后,就可以像访问属性一样访问这些服务(组件)。 前提是已经完成注册,不要求已经实例化。 访问这些服务或属性,被转换成了调用 yii\di\ServiceLocator::get() 来获取实例。 下面是使用这种形式访问服务或组件的例子:

// 创建一个Service Locator
$serviceLocator = new yii\di\ServiceLocator; // 注册一个 cache 服务
$serviceLocator->set('cache', [
'class' => 'yii\cache\MemCache',
'servers' => [
... ...
],
]); // 使用访问属性的方法访问这个 cache 服务
$serviceLocator->cache->flushValues(); // 上面的方法等效于下面这个
$serviceLocator->get('cache')->flushValues();

  

在Service Locator中,并未重载 __set() 。所以,Service Locator中的服务和组件看起来就好像只读属性一样。 要向Service Locator中“写”入服务和组件,没有 setter 可以使用,需要调用 yii\di\ServiceLocator::set() 对服务和组件进行注册。

通过Service Locator获取实例

与注册服务和组件的简单之极相反,Service Locator在创建获取服务或组件实例的过程要稍微复杂一点。 这一点和DI容器也是很像的。 Service Locator通过 yii\di\ServiceLocator::get() 来创建、获取服务或组件的实例:

public function get($id, $throwException = true)
{
// 如果已经有实例化好的组件或服务,直接使用缓存中的就OK了
if (isset($this->_components[$id])) {
return $this->_components[$id];
} // 如果还没有实例化好,那么再看看是不是已经定义好
if (isset($this->_definitions[$id])) {
$definition = $this->_definitions[$id]; // 如果定义是个对象,且不是Closure对象,那么直接将这个对象返回
if (is_object($definition) && !$definition instanceof Closure) {
// 实例化后,保存进 $_components 数组中,以后就可以直接引用了
return $this->_components[$id] = $definition; // 是个数组或者PHP callable,调用 Yii::createObject()来创建一个实例
} else {
// 实例化后,保存进 $_components 数组中,以后就可以直接引用了
return $this->_components[$id] = Yii::createObject($definition);
}
} elseif ($throwException) {
throw new InvalidConfigException("Unknown component ID: $id"); // 即没实例化,也没定义,万能的Yii也没办法通过一个任意的ID,
// 就给你找到想要的组件或服务呀,给你个 null 吧。
// 表示Service Locator中没有这个ID的服务或组件。
} else {
return null;
}
}

  

Service Locator创建获取服务或组件实例的过程是:

  • 看看缓存数组 $_components 中有没有已经创建好的实例。有的话,皆大欢喜,直接用缓存中的就可以了。
  • 缓存中没有的话,那就要从定义开始创建了。
  • 如果服务或组件的定义是个对象,那么直接把这个对象作为服务或组件的实例返回就可以了。 但有一点要注意,当使用一个PHP callable定义一个服务或组件时,这个定义是一个Closure类的对象。 这种定义虽然也对象,但是可不能把这种对象直接当成服务或组件的实例返回。
  • 如果定义是一个数组或者一个PHP callable,那么把这个定义作为参数,调用 Yii::createObject() 来创建实例。

这个 Yii::createObject() 在讲配置时我们介绍过,当时只是点一点,这里会讲得更深一点。但别急,先放一放, 知道他能为Service Locator创建对象就OK了。我们等下还会讲这个方法的。

在Yii应用中使用Service Locator和DI容器

我们在讲DI容器时,提到了Yii中是把Service Locator和DI容器结合起来用的,Service Locator是建立在DI容器之上的。 那么一个Yii应用,是如何使用Service Locator和DI容器的呢?

DI容器的引入

我们知道,每个Yii应用都有一个入口脚本 index.php 。在其中,有一行不怎么显眼:

require(__DIR__ . '/../../vendor/yiisoft/yii2/Yii.php');

这一行看着普通,也就是引入一个 Yii.php 的文件。但是,让我们来看看这个 Yii.php

<?php

require(__DIR__ . '/BaseYii.php');

class Yii extends \yii\BaseYii
{
} spl_autoload_register(['Yii', 'autoload'], true, true);
Yii::$classMap = include(__DIR__ . '/classes.php'); // 重点看这里。创建一个DI 容器,并由 Yii::$container 引用
Yii::$container = new yii\di\Container;

  

Yii 是一个工具类,继承自 yii\BaseYii 。 但这里对父类的代码没有任何重载,意味之父类和子类在功能上其实是相同的。 但是,Yii提供了让你修改默认功能的机会。 就是自己写一个 Yii 类,来扩展、重载Yii默认的、由 yii\BaseYii 提供的特性和功能。 尽管实际使用中,我们还从来没有需要改写过这个类,主要是因为没有必要在这里写代码,可以通过别的方式实现。 但Yii确实提供了这么一个可能。这个在实践中不常用,有这么个印象就足够了。

这里重点看最后一句代码,创建了一个DI容器,并由 Yii::$container 引用。 也就是说, Yii 类维护了一个DI容器,这是DI容器开始介入整个应用的标志。 同时,这也意味着,在Yii应用中,我们可以随时使用 Yii::$container 来访问DI容器。 一般情况下,如无必须的理由,不要自己创建DI容器,使用 Yii::$container 完全足够。

Application的本质

再看看入口脚本 index.php 的最后两行:

$application = new yii\web\Application($config);
$application->run();

创建了一个 yii\web\Application 实例,并调用其 run() 方法。 那么,这个 yii\web\Application 是何方神圣? 首先, yii\web\Application 继承自 yii\base\Application ,这从 yii\web\Application 的代码可以看出来

class Application extends \yii\base\Application
{
... ...
}

而 yii\base\Application 又继承自 yii\base\Module ,说明所有的Application都是Module

abstract class Application extends Module
{
... ...
}

那么 yii\base\Module 又继承自哪个类呢?不知道你猜到没,他继承自 yii\di\ServiceLocator

class Module extends ServiceLocator
{
... ...
}

所有的Module都是服务定位器Service Locator,因此,所有的Application也都是Service Locator。

同时,在Application的构造函数中, yii\base\Application::__construct()

1
2
3
4
5
public function __construct($config = [])
{
Yii::$app = $this;
... ...
}

第一行代码就把Application当前的实例,赋值给 Yii::$app 了。 这意味着Yii应用创建之后,可以随时通过 Yii::$app 来访问应用自身,也就是访问Service Locator。

至此,DI容器有了,Service Locator也出现了。那么Yii是如何摆布这两者的呢?这两者又是如何千里姻缘一线牵的呢?

实例创建方法

Service Locator和DI容器的亲密关系就隐藏在 yii\di\ServiceLocator::get() 获取实例时, 调用的 Yii::createObject() 中。 前面我们说到这个 Yii 继承自 yii\BaseYii ,因此这个函数实际上是 BaseYii::createObject() , 其代码如下:

// static::$container就是上面说的引用了DI容器的静态变量

public static function createObject($type, array $params = [])
{
// 字符串,代表一个类名、接口名、别名。
if (is_string($type)) {
return static::$container->get($type, $params); // 是个数组,代表配置数组,必须含有 class 元素。
} elseif (is_array($type) && isset($type['class'])) {
$class = $type['class'];
unset($type['class']); // 调用DI容器的get() 来获取、创建实例
return static::$container->get($class, $params, $type); // 是个PHP callable则调用其返回一个具体实例。
} elseif (is_callable($type, true)) { // 是个PHP callable,那就调用它,并将其返回值作为服务或组件的实例返回
return call_user_func($type, $params); // 是个数组但没有 class 元素,抛出异常
} elseif (is_array($type)) {
throw new InvalidConfigException(
'Object configuration must be an array containing a "class" element.'); // 其他情况,抛出异常
} else {
throw new InvalidConfigException(
"Unsupported configuration type: " . gettype($type));
}
}

这个 createObject() 提供了一个向DI容器获取实例的接口, 对于不同的定义,除了PHP callable外, createObject() 都是调用了DI容器的 yii\di\Container::get() , 来获取实例的。 Yii::createObject() 就是Service Locator和DI容器亲密关系的证明, 也是Service Locator构建于DI容器之上的证明。而Yii中所有的Module, 包括Application都是Service Locator,因此,它们也都构建在DI容器之上。

同时,在Yii框架代码中,只要创建实例,就是调用 Yii::createObject() 这个方法来实现。 可以说,Yii中所有的实例(除了Application,DI容器自身等入口脚本中实例化的),都是通过DI容器来获取的。

同时,我们不难发现, Yii 的基类 yii\BaseYii ,所有的成员变量和方法都是静态的, 其中的DI容器是个静态成员变量 $container 。 因此,DI容器就形成了最常见形式的单例模式,在内存中仅有一份,所有的Service Locator (Module和Application)都共用这个DI容器。 就就节省了大量的内存空间和反复构造实例的时间。

更为重要的是,DI容器的单例化,使得Yii不同的模块共用组件成为可能。 可以想像,由于共用了DI容器,容器里面的内容也是共享的。因此,你可以在A模块中改变某个组件的状态,而B模块中可以了解到这一状态变化。 但是,如果不采用单例模式,而是每个模块(Module或Application)维护一个自己的DI容器, 要实现这一点难度会大得多。

所以,这种共享DI容器的设计,是必然的,合理的。

另外,前面我们讲到,当Service Locator中服务或组件的定义是一个PHP callable时,对其形式有一定要求。 一是返回一个实例,二是不接收任何参数。 这在 Yii::createObject() 中也可以看出来。

由于 Yii::createObject() 为 yii\di\ServiceLocator::get() 所调用,且没有提供第二参数, 因此,当使用 Service Locator获取实例时, Yii::createObject() 的 $params 参数为空。 因此,使用 call_user_func($type, $params) 调用这个PHP callable时, 这个PHP callable是接收不到任何参数的。

Yii创建实例的全过程

可能有的读者朋友会有疑问:不对呀,前面讲过DI容器的使用是要先注册依赖,后获取实例的。 但Service Locator在注册服务、组件时,又没有向DI容器注册依赖。那在获取实例的时候, DI容器怎么解析依赖并创建实例呢?

请留意,在向DI容器索要一个没有注册过依赖的类型时, DI容器视为这个类型不依赖于任何类型可以直接创建, 或者这个类型的依赖信息容器本身可以通过Reflection API自动解析出来,不用提前注册。

可能还有的读者会想:还是不对呀,在我开发Yii的过程中,又没有写过注册服务的代码:

Yii::$app->set('db', [
'class' => 'yii\db\Connection',
'dsn' => 'mysql:host=db.digpage.com;dbname=digpage.com',
'username' => 'www.digpage.com',
'password' => 'www.digapge.com',
'charset' => 'utf8',
]); Yii::$app->set('cache', [
'class' => 'yii\caching\MemCache',
'servers' => [
[
'host' => 'cache1.digpage.com',
'port' => ,
'weight' => ,
],
[
'host' => 'cache2.digpage.com',
'port' => ,
'weight' => ,
],
],
]);

为何可以在没有注册的情况下获取服务的实例并使用服务呢?

其实,你也不是什么都没写,至少肯定是在某个配置文件中写了有关的内容的:

return [
'components' => [
'db' => [
'class' => 'yii\db\Connection',
'dsn' => 'mysql:host=localhost;dbname=yii2advanced',
'username' => 'root',
'password' => '',
'charset' => 'utf8',
],
'cache' => [
'class' => 'yii\caching\MemCache',
'servers' => [
[
'host' => 'cache1.digpage.com',
'port' => ,
'weight' => ,
],
[
'host' => 'cache2.digpage.com',
'port' => ,
'weight' => ,
],
],
],
... ...
],
];

只不过,在 配置项(Configuration) 和 Object的配置方法 部分, 我们了解了配置文件是如何产生作用的,配置到应用当中的。 这个数组会被 Yii::configure($config) 所调用,然后会变成调用Application的 setComponents(), 而Application其实就是一个Service Locator。setComponents()方法又会遍历传入的配置数组, 然后使用使用 Service Locator 的set() 方法注册服务。

到了这里,就可以了解到:每次在配置文件的 components 项写入配置信息, 最终都是在向Application这个 Service Locator注册服务。

让我们回顾一下,DI容器、Service Locator是如何配合使用的:

  • Yii 类提供了一个静态的 $container 成员变量用于引用DI容器。 在入口脚本中,会创建一个DI容器,并赋值给这个 $container 。
  • Service Locator通过 Yii::createObject() 来获取实例, 而这个 Yii::createObject() 是调用了DI容器的 yii\di\Container::get() 来向 Yii::$container 索要实例的。 因此,Service Locator最终是通过DI容器来创建、获取实例的。
  • 所有的Module,包括Application都继承自 yii\di\ServiceLocator ,都是Service Locator。 因此,DI容器和Service Locator就构成了整个Yii的基础。

服务定位器(Service Locator)的更多相关文章

  1. Atitit。如何实现dip, di ,ioc ,Service Locator的区别于联系

    Atitit.如何实现dip, di ,ioc  ,Service Locator的区别于联系 1. Dip原则又来自于松耦合思想方向1 2. 要实现dip原则,有以下俩个模式1 3. Ioc和di的 ...

  2. spring服务定位器类

    此文章是基于 搭建SpringMVC+Spring+Hibernate平台 功能:通过持有的Spring应用场景ApplicationContext,可在任何地方获取bean. 1. 服务定位器类:S ...

  3. PHP中应用Service Locator服务定位及单例模式

    单例模式将一个对象实例化后,放在静态变量中,供程序调用. 服务定位(ServiceLocator)就是对象工场Factory,调用者对象直接调用Service Locator,与被调用对象减轻了依赖关 ...

  4. Service Locator Pattern 服务定位

    https://www.geeksforgeeks.org/service-locator-pattern/ Service Locator Pattern Last Updated: 06-03-2 ...

  5. 避免在ASP.NET Core中使用服务定位器模式

    (此文章同时发表在本人微信公众号"dotNET每日精华文章",欢迎右边二维码来关注.) 题记:服务定位器(Service Locator)作为一种反模式,一般情况下应该避免使用,在 ...

  6. Service Locator 模式

    什么是Service Locator 模式? 服务定位模式(Service Locator Pattern)是一种软件开发中的设计模式,通过应用强大的抽象层,可对涉及尝试获取一个服务的过程进行封装.该 ...

  7. .NET 服务器定位模式(Service Locator Pattern)——Common Service Locator

    本文内容 场景 目标 解决方案 实现细节 思考 相关模式 更多信息 参考资料 Common Service Locator 代码很简单,它一般不会单独使用,而是作为一个单件模式,与像 .net Uni ...

  8. 依赖注入与Service Locator

    为什么需要依赖注入? ServiceUser是组件,在编写者之外的环境内被使用,且使用者不能改变其源代码. ServiceProvider是服务,其类似于ServiceUser,都要被其他应用使用,不 ...

  9. YII框架的依赖注入容器与服务定位器简述

    依赖注入容器 依赖注入(Dependency Injection,DI)容器就是一个对象use yii\di\Container,它知道怎样初始化并配置对象及其依赖的所有对象. 依赖注入和服务定位器都 ...

随机推荐

  1. HBase指定大量列集合的场景下并发拉取数据时卡住的问题排查

    最近遇到一例,HBase 指定大量列集合的场景下,并发拉取数据,应用卡住不响应的情形.记录一下. 问题背景 退款导出中,为了获取商品规格编码,需要从 HBase 表 T 里拉取对应的数据. T 对商品 ...

  2. 前端好用js库

    1.pubsub-js: PubSubJS is a topic-based publish/subscribe library written in JavaScript. 评价:做发布订阅超级简单 ...

  3. CentOS 7安装mysql(rpm)

    1.检查是否安装了mysql rpm -qa|grep -i mysql centos7默认是安装的mariadb,而安装mysql的话会和mariadb的文件冲突,所以需要先卸载掉mariadb 2 ...

  4. Java8-对map排序

    1.Java8对map按key排序 /** * @author : fengkun * @date : 19-3-10 * 内容 : Java8对map按key排序 */ public class S ...

  5. 下拉框click事件与搜索框blur事件的爱恨纠葛

    还原车祸现场 功能类似于百度搜索,搜索框输入内容,下拉框显示候选项,点击候选项就选择候选项,然后下拉框隐藏,点击外面就直接隐藏下拉框,于是我写了以下代码 //参会单位联想 $('input[name= ...

  6. shell的打印菜单

    #!/bash/bin cat  << EOF   #EOF是变量,可随便设置,但标准是EOF 1)hello world. 2)你好,世界. EOF

  7. java0424 wen 集合框架2

  8. bzoj2839 集合计数(容斥)

    2839: 集合计数 Time Limit: 10 Sec  Memory Limit: 128 MBSubmit: 883  Solved: 490[Submit][Status][Discuss] ...

  9. HBuilder 模拟器

    模拟器名称    连接默认端口夜神安卓模拟器夜神安卓模拟器     62001逍遥安卓模拟器逍遥安卓模拟器     21503BlueStacks(蓝叠安卓模拟器)BlueStacks(蓝叠安卓模拟器 ...

  10. Servlet CDI example analysis

    上下文和依赖注入(CDI)使您的对象能够自动为它们提供依赖项,而不是创建它们或将它们作为参数接收.CDI还为您管理这些依赖项的生命周期. 例如,考虑以下servlet: @WebServlet(&qu ...