菜菜鸟Zend Framework 2 不完全学习涂鸦(十三)-- 学习依赖注入

原创
2013/08/23 17:49
阅读数 2.9K

学习依赖注入

一、非常简短的介绍 Di(Dependency Injection)

依赖注入(Dependency Injection)是一个已经在 web 界讨论很多次的概念。这里的快速入门以此为目的,我们通过以下简单的代码解释依赖注入的行为:

$b = new B(new A());

以上的代码中,A 是 B的依赖(我的理解:也就是说 B 依赖于 A,如果没有 A 那么 B 也无法正常工作),A 注入到 B 里面。如果你不熟悉依赖注入的概念,这里有几篇很好的文章: Analogy(Matthew Weier O’Phinney), Learning DI(Ralph Schindler)和  Series on DI(Fabien Potencier)。


二、最简单的使用案例(两个类,一个消耗另一个)

在这个最简单的使用案例里,开发者可以有一个类(A)通过构造函数被另一个类(B)消耗。通过构造函数来进行依赖注入,这需要一个 A 类型的对象在 B 类型的对象之前实例化,以便 A 可以注入到 B 内部。

namespace My {

    class A
    {
        /* 一些有用的功能代码 */
    }

    class B
    {
        protected $a = null;
        public function __construct(A $a)
        {
            $this->a = $a;
        }
    }
}
要手动创建 B,开发者需要跟随这个工作流程,或者与下面的工作流程类似的动作
$b = new B(new A());
我的理解:
从上面类的定义代码可以知道,要实例化 B 之前,必须要先实例化 A,然后将 A 的实例化对象作为参数传递给 B 的构造函数。举个例子,有两个类,一个是登录类(Login Class),另一个是数据库类(DB Class)。在实现登录功能时,先要实例化一个数据库类的对象,然后在实例化登录类的时候把这个数据库对象作为参数传递给登录类的构造函数,这样当登录类实例化以后才能在数据库中进行用户名和密码的读取。
如果这个工作流程在你的应用程序中至始至终重复很多次,这创造了一个使用 DRY D on't R epeat Y ourself)代码的机会。有几个方法来实现这个,使用依赖注入容器是几个解决方案中的一个。Zend 的依赖注入容器 Zend\Di\Di,注意上面的使用案例没有使用配置(假设你所有的自动加载都已经正确配置)以下使用
$di = new Zend\Di\Di;
$b = $di->get('My\B'); // 将产生一个消耗 A 对象的 B 对象
此外,使用 Di::get() 方法,你要确保在随后的调用中返回完全相同的对象。要强制每个请求创建新的对象,要使用 Di::newInstance() 方法:
$b = $di->newInstance('My\B');
让我们假设,A在被创建之前请求一些配置。我们扩充了之前的使用案例(我们额外添加了第三个类)
namespace My {

    class A
    {
        protected $username = null;
        protected $password = null;
        public function __construct($username, $password)
        {
            $this->username = $username;
            $this->password = $password;
        }
    }

    class B
    {
        protected $a = null;
        public function __construct(A $a)
        {
            $this->a = $a;
        }
    }

    class C
    {
        protected $b = null;
        public function __construct(B $b)
        {
            $this->b = $b;
        }
    }

}
上面的代码,我们需要确保我们的Di能够看到类A以及一些配置值(广义的讲就是一般的标量)。要实现这个目的,我们需要和InstanceManager交互:
$di = new Zend\Di\Di;
$di->getInstanceManager()->setProperty('A', 'username', 'MyUsernameValue');
$di->getInstanceManager()->setProperty('A', 'password', 'MyHardToGuessPassword%$#');
现在我们的容器中已经有指了,当创建A时可以使用。我们的新目标是要有一个C对象,这个C对象消耗B并且依次消耗A。一样的使用场景:
$c = $di->get('My\C');
// or
$c = $di->newInstance('My\C');
足够简单了吧,但是如果我们要在调用的时候传递参数该怎么做呢?假设一个默认的Di对象( $di = new Zend\Di\Di()没有对InstanceManager进行任何配置 )我们可以这样做:
$parameters = array(
    'username' => 'MyUsernameValue',
    'password' => 'MyHardToGuessPassword%$#',
);

$c = $di->get('My\C', $parameters);
// or
$c = $di->newInstance('My\C', $parameters);
构造函数注入不是注入支持的唯一类型。其它非常受欢迎的注入方法同样被支持:setter注入。setter注入允许的使用场景和我们先前的例子差不多,除了B类,B类现在改成如下代码:
namespace My {
    class B
    {
        protected $a;
        public function setA(A $a)
        {
            $this->a = $a;
        }
    }
}
由于这个方法以“set”作为前缀并且后面跟着一个大写字母,Di知道这个方法是用在setter注入的,再次使用 $c = $di->get('C'),Di知道在需要创建一个C类型的对象时如何填写依赖关系。

创建一些其它方法来确定类之间的链接,如:接口注入,基于注释的注入

三、最简单的没有类型提示(Type-hints)的使用案例

如果你的代码美元后类型提示(Type-hints)或者使用第三方没有类型提示的代码,但需要实现依赖注入,依然可以使用Di,但你需要明确的描述你的依赖关系。为了实现这个,你需要与定义之一进行相互作用,它能够允许开发者与描述,对象和类之间的映射关系。这个特殊的定义叫BuilderDefinition,它可以和RuntimeDefinition一起工作或者替代RuntimeDefinition。

定义是Di的一部分,它试图描述类之间的关系,所以Di::newInstance() 和 Di::get() 可以知道一个特别的类/对象需要填写哪些依赖。没有配置情况下,Di将使用RuntimeDefinition,它在你的代码中使用反射和类型标签来确定依赖关系。没有类型标签,它会假设所有的依赖关系是标量或者必须的配置参数。

BuilderDefinition它可以与RuntimeDefinition一同合作(技术上讲,通过AggregateDefinition它可以与任何定义一同合作),允许你通过编程来描述对象映射。让我们来一个例子,我们上文说到的A/B/C的使用场景,我们改变了B类的代码,如下:

namespace My {
    class B
    {
        protected $a;
        public function setA($a)
        {
            $this->a = $a;
        }
    }
}
注意到,唯一的改变是setA现在不包含任何类型标签的信息

use Zend\Di\Di;
use Zend\Di\Definition;
use Zend\Di\Definition\Builder;

// Describe this class:
$builder = new Definition\BuilderDefinition;
$builder->addClass(($class = new Builder\PhpClass));

$class->setName('My\B');
$class->addInjectableMethod(($im = new Builder\InjectableMethod));

$im->setName('setA');
$im->addParameter('a', 'My\A');

// Use both our Builder Definition as well as the default
// RuntimeDefinition, builder first
$aDef = new Definition\AggregateDefinition;
$aDef->addDefinition($builder);
$aDef->addDefinition(new Definition\RuntimeDefinition);

// Now make sure the Di understands it
$di = new Di;
$di->setDefinition($aDef);

// and finally, create C
$parameters = array(
    'username' => 'MyUsernameValue',
    'password' => 'MyHardToGuessPassword%$#',
);

$c = $di->get('My\C', $parameters);

上述使用场景提供了通用的样子,你可以确保它与依赖注入容器一起工作。在一个理想世界,你所有的代码都有适当的类型提示和/或将使用映射策略,降低了大量的引导工作,需要做的就是为了拥有完整的定义,它能够实例化你可能需要的对象。


四、非常简单的编译定义的使用场景

没有进入细节,正如你想到的,PHP的核心对Di并不友善。即开即用,Di使用RuntimeDefinition通过PHP的Reflection扩展来分辨所有的类映射。事实上PHP没有真正的应用层级能都在请求之间将对象存储在内容中的能力,有个和Java和.Net解决方案类似的方法,但是这个方法要比Java和.Net中的方法低效。(Java和.Net是应用层级将对象存储在内存中的语言)

为了减少这个缺点,Zend\Di有几个功能,能够围绕依赖注入建立预编译很多高开销的任务。值得注意的是RuntimeDefinition是默认使用的,而且是唯一定义并按需查询的。其余定义的对象都是被汇集和和存储在磁盘上,这是一种高性能的方法。

理想状态下,第三方代码将携带一个预编译的定义来各种各样关系和每个类实例的参数/属性。在第三方,这个定义将被构建成部署的一部分或者包。当不是这样的情况下,你可以通过除了RuntimeDefinition之外提供的任何定义类型来创建这些定义。这里是每个定义类型分解工作:

  • AggregateDefinition - Aggregates多重定义各种各样的类型。当查找一个类时,它按顺序将定义提供给Aggregate
  • ArrayDefinition - 这个定义取出一个数组的信息并且通过Zend\Di\Definition提供的接口展示出来,适合Di或者一个AggregateDefinition的场景.
  • BuilderDefinition - 创建一个基于包含各种对象图Builder\PhpClass对象和Builder\InjectionMethod对象描述映射需要的目标代码库的定义
  • Compiler - 这实际上不是一个定义,但它是ArrayDefinition产生过程中基于的一个代码扫描器(Zend\Code\Scanner\DirectoryScanner或者Zend\Code\Scanner\FileScanner

下面是一个通过DirectoryScanner产生定义的过程例子

$compiler = new Zend\Di\Definition\Compiler();
$compiler->addCodeScannerDirectory(
    new Zend\Code\Scanner\ScannerDirectory('path/to/library/My/')
);
$definition = $compiler->compile();
这个定义可以直接的使用Di(假设以上A、B、C场景中每个类保存为磁盘上的一个文件)
$di = new Zend\Di\Di;
$di->setDefinition($definition);
$di->getInstanceManager()->setProperty('My\A', 'username', 'foo');
$di->getInstanceManager()->setProperty('My\A', 'password', 'bar');
$c = $di->get('My\C');
一种坚持编译定义的策略如下
if (!file_exists(__DIR__ . '/di-definition.php') && $isProduction) {
    $compiler = new Zend\Di\Definition\Compiler();
    $compiler->addCodeScannerDirectory(
        new Zend\Code\Scanner\ScannerDirectory('path/to/library/My/')
    );
    $definition = $compiler->compile();
    file_put_contents(
        __DIR__ . '/di-definition.php',
        '<?php return ' . var_export($definition->toArray(), true) . ';'
    );
} else {
    $definition = new Zend\Di\Definition\ArrayDefinition(
        include __DIR__ . '/di-definition.php'
    );
}

// $definition can now be used; in a production system it will be written
// to disk.
因为 Zend\Code\Scanner不包含文件,内部包含的类没有调用到内存中。相反 Zend\Code\Scanner使用标记化来确定你文件的结构。这使它能适当的在开发和 在相同的请求内部使用,相同的请求时 随着你任何一个应用程序的派遣的action。

五、创建一个预编译定义供别人使用

如果你是第三方开发人员,产生一个定义文件来描述你的代码是有意义的,他人可以利用这个定义而不必通过RuntimeDefinition来Reflect它,或者通过Compiler来创建它。要这么做,使用上面说到的技巧。而不是在磁盘上写结果数组,直接使用Zend\Code\Generator方法将信息写入一个定义

// First, compile the information
$compiler = new Zend\Di\Definition\CompilerDefinition();
$compiler->addDirectoryScanner(
    new Zend\Code\Scanner\DirectoryScanner(__DIR__ . '/My/')
);
$compiler->compile();
$definition = $compiler->toArrayDefinition();

// Now, create a Definition class for this information
$codeGenerator = new Zend\Code\Generator\FileGenerator();
$codeGenerator->setClass(($class = new Zend\Code\Generator\ClassGenerator()));
$class->setNamespaceName('My');
$class->setName('DiDefinition');
$class->setExtendedClass('\Zend\Di\Definition\ArrayDefinition');
$class->addMethod(
    '__construct',
    array(),
    \Zend\Code\Generator\MethodGenerator::FLAG_PUBLIC,
    'parent::__construct(' . var_export($definition->toArray(), true) . ');'
);
file_put_contents(__DIR__ . '/My/DiDefinition.php', $codeGenerator->generate());

六、使用来自多源的多定义

在所有的现实中,你使用来自于不同地方的代码,一些ZF代码,一些第三方的代码,当然还有你自己的代码来构成你的应用程序。这里有一个来自于多个地方消耗定义的方法:

use Zend\Di\Di;
use Zend\Di\Definition;
use Zend\Di\Definition\Builder;

$di = new Di;
$diDefAggregate = new Definition\Aggregate();

// first add in provided Definitions, for example
$diDefAggregate->addDefinition(new ThirdParty\Dbal\DiDefinition());
$diDefAggregate->addDefinition(new Zend\Controller\DiDefinition());

// for code that does not have TypeHints
$builder = new Definition\BuilderDefinition();
$builder->addClass(($class = Builder\PhpClass));
$class->addInjectionMethod(
    ($injectMethod = new Builder\InjectionMethod())
);
$injectMethod->setName('injectImplementation');
$injectMethod->addParameter(
'implementation', 'Class\For\Specific\Implementation'
);

// now, your application code
$compiler = new Definition\Compiler()
$compiler->addCodeScannerDirectory(
    new Zend\Code\Scanner\DirectoryScanner(__DIR__ . '/App/')
);
$appDefinition = $compiler->compile();
$diDefAggregate->addDefinition($appDefinition);

// now, pass in properties
$im = $di->getInstanceManager();

// this could come from Zend\Config\Config::toArray
$propertiesFromConfig = array(
    'ThirdParty\Dbal\DbAdapter' => array(
        'username' => 'someUsername',
        'password' => 'somePassword'
    ),
    'Zend\Controller\Helper\ContentType' => array(
        'default' => 'xhtml5'
    ),
);
$im->setProperties($propertiesFromConfig);

七、生成服务定位器

在生产中,你希望尽可能的运行的快。依赖注入容器是为速度而设计的,还需要做一个公平点的工作,解决运行时的参数和依赖性。什么是可以加速和删除的查找呢?

Zend\Di\ServiceLocator\Generator组件可以做到。它需要一个Di配置实例而且为你生成一个服务定位器类,这个类为你管理实例以及提供硬编码,延迟加载实例化的实例。

getCodeGenerator()方法返回一个Zend\CodeGenerator\Php\PhpFile的实例,然后你就可以写一个类文件与新的服务定位器。在Generator中的方法允许你指定命名空间和类生成的服务定位器

作为一个例子,考虑以下几点:

use Zend\Di\ServiceLocator\Generator;

// $di is a fully configured DI instance
$generator = new Generator($di);

$generator->setNamespace('Application')
          ->setContainerClass('Context');
$file = $generator->getCodeGenerator();
$file->setFilename(__DIR__ . '/../Application/Context.php');
$file->write();
以上的代码将放在 ../Application/Context.php中并且包含 Application\Context类。文件可能看上去如下:

<?php

namespace Application;

use Zend\Di\ServiceLocator;

class Context extends ServiceLocator
{

    public function get($name, array $params = array())
    {
        switch ($name) {
            case 'composed':
            case 'My\ComposedClass':
                return $this->getMyComposedClass();

            case 'struct':
            case 'My\Struct':
                return $this->getMyStruct();

            default:
                return parent::get($name, $params);
        }
    }

    public function getComposedClass()
    {
        if (isset($this->services['My\ComposedClass'])) {
            return $this->services['My\ComposedClass'];
        }

        $object = new \My\ComposedClass();
        $this->services['My\ComposedClass'] = $object;
        return $object;
    }
    public function getMyStruct()
    {
        if (isset($this->services['My\Struct'])) {
            return $this->services['My\Struct'];
        }

        $object = new \My\Struct();
        $this->services['My\Struct'] = $object;
        return $object;
    }

    public function getComposed()
    {
        return $this->get('My\ComposedClass');
    }

    public function getStruct()
    {
        return $this->get('My\Struct');
    }
}
要使用这个类,你就像使用一个Di容器那样简单的使用。

$container = new Application\Context;

$struct = $container->get('struct'); // My\Struct instance
在当前的案例中有一个注意的功能。每个配置环境只在当前有效:意思是说你需要在每个执行环境中产生一个容器。我们的建议是,你按照这样做,在你的环境中使用指定的容器类。



未完待续,谢谢......


展开阅读全文
打赏
2
2 收藏
分享
加载中
更多评论
打赏
0 评论
2 收藏
2
分享
返回顶部
顶部