Php 依赖注入,其中虚拟的每个类都依赖于其他几个类

Php 依赖注入,其中虚拟的每个类都依赖于其他几个类,php,oop,dependency-injection,Php,Oop,Dependency Injection,我试图在学习PHP OOP的同时实现最佳实践。我理解这个概念,但对正确实施有点怀疑。当我试图找出基本的实现原理时,我并没有在这段代码中实现DI容器 结构 数据库连接的Db类 设置类,从数据库检索设置 语言类,检索特定语言的信息 页面类、产品类、客户类,以及更多 主意 设置类需要Db类来检索设置 语言类需要Db和设置来根据数据库中的设置检索信息 页面类需要Db、设置和语言。将来可能还需要一些其他的课程 简化代码 php扩展了PDO Settings.php class Settings {

我试图在学习PHP OOP的同时实现最佳实践。我理解这个概念,但对正确实施有点怀疑。当我试图找出基本的实现原理时,我并没有在这段代码中实现DI容器

结构
  • 数据库连接的Db类

  • 设置类,从数据库检索设置

  • 语言类,检索特定语言的信息

  • 页面类、产品类、客户类,以及更多

主意 设置类需要Db类来检索设置

语言类需要Db设置来根据数据库中的设置检索信息

页面类需要Db设置语言。将来可能还需要一些其他的课程

简化代码 php扩展了PDO

Settings.php

class Settings
{
    /* Database instance */
    protected $db;

    /* Cached settings */
    private $settings   = array();

    public function __construct(Db $db)
    {
        $this->db = $db;
    }

    public function load ()
    {
        $selq = $this->db->query('SELECT setting, value FROM settings');
        $this->settings = $selq->fetchAll();
    }
}
Languages.php

class Languages
{

    public $language;

    protected $db;
    protected $settings;

    private $languages = array();

    public function __construct(Db $db, Settings $settings)
    {
        $this->db = $db;
        $this->settings = $settings;
        // set value for $this->language based on user choice or default settings
        ...
    }

    public function load() 
    {
        $this->languages = array();
        $selq = $this->db->query('SELECT * FROM languages');
        $this->languages = $selq->fetchAll();
    }

}
Page.php

class Page
{
    protected $db;
    protected $settings;
    protected $language;

    public function __construct(Db $db, Settings $settings, Languages $languages)
    {
        $this->db = $db;
        $this->settings = $settings;
        $this->languages = $languages;
    }

    public function load() 
    {
        // load page info from db with certain settings and in proper language
        ...
    }

}
Config.php

$db = new Db;

/* Load all settings */
$settings   = new Settings($db);
$settings->load();

/* Load all languages */
$languages  = new Languages($db, $settings);
$languages->load();

/* Instantiate page */
$page   = new Page($db, $settings, $languages);
$db = new Db;

/* Load all settings */
$settings   = new Settings($db);
$settings->load();

/* Load all languages */
$languages  = new Languages($settings);
$languages->load();

/* Instantiate page */
$page   = new Page($languages);
我不喜欢一遍又一遍地给同一个类注入信息的想法。这样,我就可以开始了,我需要注入10个类。所以,我的代码从一开始就是错误的

也许,更好的方法是执行以下操作:

Config.php

$db = new Db;

/* Load all settings */
$settings   = new Settings($db);
$settings->load();

/* Load all languages */
$languages  = new Languages($db, $settings);
$languages->load();

/* Instantiate page */
$page   = new Page($db, $settings, $languages);
$db = new Db;

/* Load all settings */
$settings   = new Settings($db);
$settings->load();

/* Load all languages */
$languages  = new Languages($settings);
$languages->load();

/* Instantiate page */
$page   = new Page($languages);
as设置已经可以访问$db,并且$languages可以访问$db和$Settings。 但是,以这种方式,我必须调用$this->languages->settings->db->

我所有的代码架构似乎都是完全错误的:)
应该如何做?

如果您在另一个对象中使用一个对象的依赖关系,那么您将在这两个组件之间创建一个依赖关系。这意味着,如果您需要更改
设置
类的依赖项,它会破坏依赖于
设置
类的所有内容

依赖项注入库

如果您担心每次都必须手动构建新对象,请查看依赖项注入库以处理对象创建(如或)

使用粉刺:

// Define this once in your bootstrap
use Pimple\Container;
$container = new Container()

$container['Db'] = function ($c) {
    return new Db();
};

$container['Settings'] = function ($c) {
    return new Settings($c['Db']);
};

$container['Languages'] = function ($c) {
    return new Languages($c['Db'], $c['Settings']);
};

$container['Page'] = function ($c) {
    return new Page($c['Db'], $c['Settings'], $c['Languages']);
};


// Where ever you have access to your $container you can use this
// (and it knows how to build your object for you every time).
$page = $container['Page'];
使用访问器注入依赖项

您可以使用访问器函数设置依赖项:

class Settings {
    protected $db;

    function setDb(Db $db) {
        $this->db = $db;
    }

    // ...
}

$settings = new Settings();
$settings->setDb(new Db());
支持使用访问器进行依赖项注入

use Aura\Di\Container;
use Aura\Di\Factory;

$di = new Container(new Factory());
$di->set('db', new Db());
$di->set('settings', new Settings());
$di->setter['settings']['setDb'] = $di->get('db');
您可以进一步扩展它,并通过基于扩展的接口进行注入,自动注入不希望在每个类(如a)上手动设置的公共依赖项

use Aura\Di\Container;
use Aura\Di\Factory;
use Psr\Log\LoggerAwareTrait;

class Db {
    use LoggerAwareTrait;
    // ...
}

$di = new Container(new Factory());
$di->set('logger', new MyCustomLogger());
$di->set('db', new Db());
$di->setter['LoggerAwareTrait']['setLogger'] = $di->get('logger');

// $db->logger will contain an instance of MyCustomLogger
// so will any other class that uses LoggerAwareTrait
$db = $di->get('db');
单一责任原则

当然,如果您的类有10个不同的依赖项,那么问题可能在于设计。一个班级应该有一个学生

可能违反单一责任的类的症状 原则:

A -> BI
A2 -> BI2
B impl BI
B impl BI2
  • 该类有许多实例变量
  • 该类有许多公共方法
  • 类的每个方法都使用其他实例变量
  • 特定任务被委托给私有方法
来自马蒂亚斯·诺巴克的“”一文


我将尝试回答我自己的问题,因为我在学习了大量的材料后看到了这一点

1。最佳做法是创建如下对象:

$db = new Db();

$settings = new Settings ($db);

$languages = new Languages ($db, $settings);

// ...
2。使用DI容器。

如果您不能编写一个,请使用现有的一个。有一些称自己为DI容器而不是它们的人,比如Pimple(在这个网站上有一些关于这个的帖子)。有些程序比其他程序慢得多、复杂得多(Zend、Symfony),但也提供了更强大的功能。如果您正在阅读本文,那么您可能应该选择更简单的一个,如Aura、Auryn、Dice、PHP-DI(按字母顺序)。 同样重要的是要知道,适当的DI容器(在我看来)应该能够递归地遍历依赖项,这意味着找到特定对象所需的依赖项。它们还应该提供共享相同对象的功能(如$db实例)

3。手动注入依赖项将在尝试动态创建对象时给您带来许多问题(如果您使用前端控制器和路由)。这就是为什么见第2点

请参见此处的示例:

要观看的视频:

$db = new Db();

$settings = new Settings ($db);

$languages = new Languages ($db, $settings);

// ...

(这不是PHP,但试着去理解它)

有一些想法可以摆脱那些丑陋的链依赖关系。想想三个具体的类A,B,C,其中A需要B,B需要C,所以隐式的A需要C。这是非常糟糕的软件设计。 测试更加复杂,因为

  • 如果想要进行一些集成测试,您需要这三个对象
  • 如果您想测试一个隔离的,那么您需要对B进行模拟并禁用ctor,或者还必须模拟C
  • 此时,您应该停止您的设计。要将A与C解耦,您应该创建一个接口BIA依赖于B实现

    发件人:

    我们现在有:

    A -> BI
    B impl BI
    B -> C
    
    我们可以使用另一个接口CI将它们完全解耦:

    A -> BI
    B impl BI
    B -> CI
    C impl CI
    
    现在我们的具体类是解耦的,但是如果我们像分离它们一样连接它们(例如通过DI),那么就没有利润了。因此,下一步是将接口缩小为仅依赖于BI的类,而BI仅具有a所需的方法

    假设这样做了,我们需要一个类A2,它需要一些B的方法。你会发现实际上BI对A2很好,因为BI有一些方法A2需要(我们现在不考虑C和CI):

    几天后,您发现我们的接口BI实际上对于A来说太大了,您希望重构BI以使BI尽可能小。但是您不能重构它,因为A2使用了一些您想要删除的方法

    这也可能是糟糕的设计。我们现在可以做的是(接口隔离原则):