Контейнер внедрения зависимостей для объекта, который содержит объект, который внедрил зависимость

Используя pimple в качестве контейнера внедрения зависимостей, я смело рефакторил небольшие классы, чтобы полагаться на внедрение внедрения зависимостей, устраняя жестко закодированные зависимости, которые я мог видеть, будут легко удалены.

Моя методология для этой задачи очень проста, но я понятия не имею, правильна ли она, поскольку у меня очень мало опыта в области внедрения зависимостей и модульного тестирования, если не считать того, что я узнал здесь за последний месяц.

Я создал класс ContainerFactory, который является подклассом pimple, и в этом подклассе создал методы, которые просто возвращают контейнер для определенного объекта.

Конструктор вызывает правильный метод создателя в зависимости от типа:

function __construct($type=null, $mode = null){

 if(isset($type)){  
    switch ($type) {
      case 'DataFactory':
         $this->buildDataFactoryContainer($mode);     
        break;
      case 'DbConnect':
         $this->buildDbConnectContainer($mode);  
        break;
     default:
        return false;
    }
  }
}

Сигнатура метода для создания объекта-контейнера выглядит следующим образом:

public function buildDataFactoryContainer($mode=null)

Идея состоит в том, что я могу установить $mode для тестирования при вызове этого контейнера и загрузить тестовые значения вместо фактических настроек времени выполнения. Я хотел избежать написания отдельных классов контейнеров для тестирования, и я думал, что это простой способ не иметь кода, связанного с тестами, повсюду.

Вместо этого я мог бы создать подкласс ContainerFactory, т.е.: ContainerFactoryTesting extends ContainerFactory, и переопределить его вместо того, чтобы смешивать тестовый код с кодом приложения и загромождать сигнатуры методов с помощью $mode=null, но это не является целью этого поста. Двигаясь дальше, чтобы создать контейнер для определенного объекта, я просто делаю это:

 // returns container with DataFactory dependencies, holds $db and $logger objects.
 $dataFactoryContainer = new ContainerFactory('DataFactory');

// returns container with test settings.
$dataFactoryTestContainer = new ContainerFactory('DataFactory','test');

// returns container with DbConnect dependencies, holds dbconfig and $logger objects.
$dbConnectContainer = new ContainerFactory('DbConnect');

Я только что столкнулся с проблемой, которая заставляет меня подозревать, что стратегия, на которой я строю, ошибочна.

Глядя на вышеизложенное, DataFactory содержит объект $db, который содержит соединения с базой данных. Сейчас я реорганизую этот dbclass, чтобы удалить его зависимости от объекта $registry, но как мне создать $dataFactoryContainer, когда я добавлю объект $db, которому нужен $dbConnectContainer?

Например, в контейнере datafactory я добавляю экземпляр dbconnect, но теперь ИТ-специалистам потребуется переданный ему контейнер...

Я понимаю, что мой английский не так уж хорош, и надеюсь, что объяснил достаточно хорошо, чтобы товарищ SO'er понял.

Мой вопрос состоит из двух частей: как вы, ребята, справляетесь с созданием объектов для зависимостей, которые сами содержат зависимости, простым способом?

И... как вы разделяете конфигурацию контейнера для создания объектов в целях тестирования?

Как всегда, любой комментарий или ссылка на соответствующий пост приветствуются.


person stefgosselin    schedule 26.05.2011    source источник
comment
Я несколько решил проблему, создав свой объект db в методе создания datafactoryContainer: $dbConnect = isset($dbConnect)? $dbConnect : DbConnect::getInstance(новый ContainerFactory('DbConnect'));   -  person stefgosselin    schedule 26.05.2011
comment
Но я почти уверен, что это неправильно использует контейнер DI.   -  person stefgosselin    schedule 26.05.2011
comment
Вы должны внедрить DataFactory в конструктор, который вы показали. Этот код с блоком переключателей не очень похож на DI.   -  person Ondřej Mirtes    schedule 26.05.2011
comment
Это мое имя :) Я рекомендую вам прочитать статьи Misko Heverys, которые являются отличным руководством по написанию чистого и тестируемого кода. misko.hevery.com/code-reviewers-guide   -  person Ondřej Mirtes    schedule 26.05.2011
comment
@Ondřej Mirtes - К вашему сведению, ваше имя пользователя не набирается в большинстве стран мира! (усмехается). Что касается переключателя наверху, он действительно не казался мне правильным, когда я его собрал. Игорь отвечает ниже, и перечитав страницу документации по прыщам еще несколько раз, я полностью изменил свой подход. Теперь все объекты инициализируются замыканиями в одном контейнере, а не «жестко закодированы» в нескольких контейнерах. Из-за тривиальной ошибки в pimple, только на windows я не мог понять правильный сценарий использования. После показа Игорю прыщ залатали за считанные минуты, и теперь я использую его по назначению.   -  person stefgosselin    schedule 26.05.2011


Ответы (1)


Вы не должны создавать отдельные контейнеры для всего, а вместо этого использовать один контейнер. Вы можете создать контейнер «Расширение», которое в основном просто устанавливает службы в вашем контейнере.

Вот обширный пример предложенной мной настройки. Когда у вас есть один контейнер, рекурсивное разрешение зависимостей тривиально. Как видите, security.authentication_provider зависит от db, которое зависит от db.config.

Из-за ленивых замыканий легко определить службы, а затем определить их конфигурацию. Кроме того, их также легко переопределить, как вы можете видеть с TestExtension.

Определения службы можно было бы разделить на несколько отдельных расширений, чтобы сделать их более удобными для повторного использования. Это в значительной степени то, что делает микрофреймворк Silex (он использует шипы).

Надеюсь, это ответит на ваши вопросы.

Интерфейс расширений

class ExtensionInterface
{
    function register(Pimple $container);
}

Расширение приложения

class AppExtension
{
    public function register(Pimple $container)
    {
        $container['mailer'] = $container->share(function ($container) {
            return new Mailer($container['mailer.config']);
        });

        $container['db'] = $container->share(function ($container) {
            return new DatabaseConnection($container['db.config']);
        });

        $container['security.authentication_provider'] = $container->share(function ($container) {
            return new DatabaseAuthenticationProvider($container['db']);
        });
    }
}

Расширение конфигурации

class ConfigExtension
{
    private $config;

    public function __construct($configFile)
    {
        $this->config = json_decode(file_get_contents($configFile), true);
    }

    public function register(Pimple $container)
    {
        foreach ($this->config as $name => $value) {
            $container[$name] = $container->protect($value);
        }
    }
}

config.json

{
    "mailer.config": {
        "username": "something",
        "password": "secret",
        "method":   "smtp"
    },
    "db.config": {
        "username": "root",
        "password": "secret"
    }
}

Расширение теста

class TestExtension
{
    public function register(Pimple $container)
    {
        $container['mailer'] = function () {
            return new MockMailer();
        };
    }
}

Фабрика контейнеров

class ContainerFactory
{
    public function create($configDir)
    {
        $container = new Pimple();

        $extension = new AppExtension();
        $extension->register($container);

        $extension = new ConfigExtension($configDir.'/config.json');
        $extension->register($container);

        return $container;
    }
}

Приложение

$factory = new ContainerFactory();
$container = $factory->create();

Проверить начальную загрузку

$factory = new ContainerFactory();
$container = $factory->create();

$extension = new TestExtension();
$extension->register($container);
person igorw    schedule 26.05.2011
comment
Игорь! Какой это маленький мир. Я искал способ связаться с вами по электронной почте, и вот вы отвечаете на мой ТАКОЙ вопрос! Странная уверенность! Я был на вашей учетной записи github, приземлился там, ища информацию о прыщах. Я сделал ментальную проверку, чтобы вернуться к силексу, так как он выглядит очень интересно. В документации есть пример, который генерирует фатальную ошибку, когда я пытался воссоздать его с помощью фиктивных классов. Я нашел решение обернуть с помощью protected(), но я до сих пор не понимаю одну или две концепции, касающиеся прыщей, я действительно хотел бы, чтобы у меня как-то была ваша электронная почта, но ТАК лучше, чем ничего. - person stefgosselin; 26.05.2011
comment
Спасибо, что нашли время, и отличный ответ. Очень признателен М. Видлеру - person stefgosselin; 26.05.2011
comment
Этот пост станет отличным дополнением к существующей документации по pimple. Доступная документация неплохая, но переопределение для тестирования не было ясным, равно как и использование объекта с зависимостью внутри другого объекта. И ContainerFactory, которая связывает все вместе, — настоящая вишенка на торте. Это в значительной степени ответило на все мои вопросы «как заставить Pimple ладить с этим приложением». Мои 2 цента и респект вам. PS: Фиксация, которую вы выдвинули сегодня, прекрасно работает с моей стороны. Спасибо. - person stefgosselin; 26.05.2011
comment
Да, было бы неплохо получить еще немного документации по Pimple. В документации по Silex многое описано: silex-project.org/doc/services.html Практически все из этой главы применимо и к Pimple. - person igorw; 26.05.2011