用PHP反转控制

时间:2014-05-15 20:30:18

标签: php design-patterns dependency-injection inversion-of-control

我刚开始使用依赖注入时出于显而易见的原因,并且在没有阅读控制反转(IoC)的情况下很快发现了在实例化我的某些类时出现冗长的问题。所以,阅读IoC我有一个问题没有找到具体的答案。课程注册何时发生?在引导程序?执行前?如何强制执行依赖项的类型?

我没有使用任何框架。为了学习,我写了自己的容器。

这是我的容器和一些示例类的一个非常低级的例子。

class DepContainer
{
    private static $registry = array();

    public static function register($name, Closure $resolve)
    {
        self::$registry[$name] = $resolve;
    }

    public static function resolve($name)
    {
        if (self::registered($name)) {
            $name = static::$registry[$name];
            return $name();
        }
        throw new Exception('Nothing bro.');
    }

    public static function registered($name)
    {
        return array_key_exists($name, self::$registry);
    }
}

class Bar
{
    private $hello = 'hello world';

    public function __construct()
    {
        # code...
    }

    public function out()
    {
        echo $this->hello . "\n";
    }
}

class Foo
{
    private $bar;

    public function __construct()
    {
        $this->bar = DepContainer::resolve('Bar');
    }

    public function say()
    {
        $this->bar->out();
    }
}

这些已经在app结构中了。 Dependecy Injection方式我会输入提示输入参数,但没有它我可以做:

DepContainer::register('Bar', function(){
    return new Bar();
});

$f = new Foo();
$f->say();

对我来说,在一个bootsrap寄存器中有意义的所有依赖关系,它将是IMO更干净的方式。在运行时,就像你所表明的那样,我认为和new Foo(new Bar(...)...)一样丑陋。

1 个答案:

答案 0 :(得分:1)

我将尝试总结一些你应该知道的事情,(希望)将澄清你的一些困境。

让我们从一个基本的例子开始:

class MySQLAdapter
{
    public function __construct()
    {
        $this->pdo = new PDO();
    }
}

class Logger
{
    public function __construct()
    {
        $this->adapter = new MySqlAdapter();
    }
}

$log = new Logger();

如您所见,我们正在实例化Logger,它有两个依赖项:MySQLAdapter和PDO。

此过程的工作方式如下:

  • 我们创建了Logger
    • Logger创建MySQLAdapter
      • MySQLAdapter创建PDO

上面的代码有效,但如果明天我们决定需要将数据记录在文件而不是数据库中,我们需要 更改Logger课程并将MySQLAdapter替换为全新的FileAdapter

// not good
class Logger
{
    public function __construct()
    {
        $this->adapter = new FileAdapter();
    }
}

这是依赖注入尝试解决的问题:因为依赖关系已更改而不修改类

依赖注入

Di引用了一个实例化类的过程,它通过赋予它构造函数所需的所有依赖关系来正常运行。如果我们应用依赖 注入我们之前的示例,它将如下所示:

interface AdapterInterface
{
}

class FileAdapter implements AdapterInterface
{
    public function __construct()
    {
    }
}

class MySQLAdapter implements AdapterInterface
{
    public function __construct(PDO $pdo)
    {
        $this->pdo = $pdo;
    }
}

class Logger
{
    public function __construct(AdapterInterface $adapter)
    {
        $this->adapter = $adapter;
    }
}

// log to mysql
$log = new Logger(
    new MySQLAdapter(
        new PDO()
    )
);

正如您所看到的,我们没有在构造函数中实例化任何内容,但我们将实例化的类传递给构造函数。这允许我们替换任何依赖 没有修改类:

// log to file
$log = new Logger(
    new FileAdapter()
);

这有助于我们:

  1. 轻松维护代码: 正如您已经看到的,如果其中一个依赖项发生了变化,我们就不需要修改该类。

  2. 使代码更易于测试: 当您针对MySQLAdapter运行测试套件时,您不希望在每次测试时都访问数据库,因此PDO对象在测试中将为mocked

    // test snippet
    $log = new Logger(
        new MySQLAdapter(
            $this->getMockClass('PDO', [...])
        )
    );
    
  3. 问: Logger如何知道你给他一个它需要的课而不是一些垃圾?
    A:这是接口( AdapterInterface )作业,它是Logger与其他类之间的合同。记录器"知道"任何实现的类  该特定界面将包含完成其工作所需的方法。

    依赖注入容器:

    您可以将此类( ie:container )视为存储运行应用程序所需的所有对象的中心位置。当你需要其中一个, 您从容器中请求对象而不是实例化自己。

    你可以把DiC视为一只受过训练的狗出去,拿到报纸并把它带回给你。问题是,只有在前门打开的情况下训练了狗。 只要狗的依赖关系不会改变(即开门),一切都会好起来的。如果有一天前门将被关闭,狗将不知道如何拿到报纸。

    但如果狗有一个IoC容器,他可以找到一种方法......

    控制倒置

    正如你所看到的那样," classic"的初始化过程代码是:

    • 我们创建了Logger
      • Logger创建MySQLAdapter
        • MySQLAdapter创建PDO

    IoC简单地复制了上述过程,但顺序相反:

    • 创建PDO
      • 创建MySQLAdapter并给他PDO
        • 创建记录器并给他MySQLAdapter

    如果您认为依赖注入是某种IoC,那么您是对的。当我们谈到依赖注入时,我们有了这个例子:

    // log to mysql
    $log = new Logger(
        new MySQLAdapter(
            new PDO()
        )
    );
    

    初看起来有人可以说实例化过程是:

    • 创建记录器
    • 创建MySQLAdapter
    • 创建PDO`

    问题是代码将从中间向左侧解释。所以订单将是:

    • 创建PDO
      • 创建MySQLAdapted并给他PDO
        • 创建记录器并给他MySQLAdapter

    IoC容器只是自动执行此过程。当您从容器中请求Logger时,它使用PHP Reflectiontype hinting来分析其依赖项( from constructor ),实例化所有这些,将它们发送到请求的类,并返回一个Logger实例。

    注意:要找出类具有哪些依赖项,一些IoC容器使用annotations而不是类型提示或两者的组合。

    所以回答你的问题:

    • 如果容器可以自行解决依赖关系,则只需要在启动期间实例化容器 你的申请流程。 (参见控制容器的反转
    • 如果容器不能自行解决依赖关系,则需要手动为容器配置运行应用程序所需的对象。此配置通常在引导过程中发生。 (参见依赖注入容器)

    如果您的容器可以自行解决依赖关系,但由于各种原因您还需要手动添加更多依赖关系,您可以在 初始化容器后的引导过程。

    注意: 在野外这两个原则之间有各种各样的混合,但我试着向你解释每个原则背后的主要思想是什么。 容器的外观如何仅取决于您,并且不要害怕重新发明轮子,只要您出于教育目的而这样做。