Spec-Zone.ru › Phalcon 3

Объяснение инъекции зависимостей

Следующий пример немного длинный, но он пытается объяснить, почему Phalcon использует расположение сервисов и инъекцию зависимостей. Сначала давайте представим, что мы разрабатываем компонент под названием SomeComponent. Он выполняет задачу, которая сейчас не важна. Наш компонент имеет зависимость — подключение к базе данных.

В этом первом примере подключение создается внутри компонента. Этот подход непрактичен; из-за того, что мы не можем изменить параметры подключения или тип системы баз данных, потому что компонент работает только так, как был создан.

class SomeComponent
{
    /**
     * The instantiation of the connection is hardcoded inside
     * the component, therefore it's difficult replace it externally
     * or change its behavior
     */
    public function someDbTask()
    {
        $connection = new Connection(
            [
                "host"     => "localhost",
                "username" => "root",
                "password" => "secret",
                "dbname"   => "invo",
            ]
        );

        // ...
    }
}

$some = new SomeComponent();

$some->someDbTask();

Чтобы решить эту проблему, мы создали метод-сеттер, который инжектирует зависимость внешне, прежде чем использовать ее. Пока это кажется хорошим решением:

class SomeComponent
{
    protected $_connection;

    /**
     * Sets the connection externally
     */
    public function setConnection($connection)
    {
        $this->_connection = $connection;
    }

    public function someDbTask()
    {
        $connection = $this->_connection;

        // ...
    }
}

$some = new SomeComponent();

// Create the connection
$connection = new Connection(
    [
        "host"     => "localhost",
        "username" => "root",
        "password" => "secret",
        "dbname"   => "invo",
    ]
);

// Inject the connection in the component
$some->setConnection($connection);

$some->someDbTask();

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

class Registry
{
    /**
     * Returns the connection
     */
    public static function getConnection()
    {
        return new Connection(
            [
                "host"     => "localhost",
                "username" => "root",
                "password" => "secret",
                "dbname"   => "invo",
            ]
        );
    }
}

class SomeComponent
{
    protected $_connection;

    /**
     * Sets the connection externally
     */
    public function setConnection($connection)
    {
        $this->_connection = $connection;
    }

    public function someDbTask()
    {
        $connection = $this->_connection;

        // ...
    }
}

$some = new SomeComponent();

// Pass the connection defined in the registry
$some->setConnection(Registry::getConnection());

$some->someDbTask();

Теперь давайте представим, что нам нужно реализовать два метода в компоненте, первый всегда должен создавать новое подключение, а второй всегда должен использовать общее подключение:

class Registry
{
    protected static $_connection;

    /**
     * Creates a connection
     */
    protected static function _createConnection()
    {
        return new Connection(
            [
                "host"     => "localhost",
                "username" => "root",
                "password" => "secret",
                "dbname"   => "invo",
            ]
        );
    }

    /**
     * Creates a connection only once and returns it
     */
    public static function getSharedConnection()
    {
        if (self::$_connection === null) {
            self::$_connection = self::_createConnection();
        }

        return self::$_connection;
    }

    /**
     * Always returns a new connection
     */
    public static function getNewConnection()
    {
        return self::_createConnection();
    }
}

class SomeComponent
{
    protected $_connection;

    /**
     * Sets the connection externally
     */
    public function setConnection($connection)
    {
        $this->_connection = $connection;
    }

    /**
     * This method always needs the shared connection
     */
    public function someDbTask()
    {
        $connection = $this->_connection;

        // ...
    }

    /**
     * This method always needs a new connection
     */
    public function someOtherDbTask($connection)
    {

    }
}

$some = new SomeComponent();

// This injects the shared connection
$some->setConnection(
    Registry::getSharedConnection()
);

$some->someDbTask();

// Here, we always pass a new connection as parameter
$some->someOtherDbTask(
    Registry::getNewConnection()
);

До сих пор мы видели, как инъекция зависимостей решила наши проблемы. Передача зависимостей в качестве аргументов вместо их создания внутри кода делает наше приложение более поддерживаемым и детерминированным. Однако в долгосрочной перспективе у этой формы инъекции зависимостей есть некоторые недостатки.

Например, если компонент имеет много зависимостей, нам нужно будет создать несколько аргументов-сеттеров для передачи зависимостей или создать конструктор, который передаст их со многими аргументами, а также создавать зависимости перед использованием компонента каждый раз, что делает наш код не таким поддерживаемым, как хотелось бы:

// Create the dependencies or retrieve them from the registry
$connection = new Connection();
$session    = new Session();
$fileSystem = new FileSystem();
$filter     = new Filter();
$selector   = new Selector();

// Pass them as constructor parameters
$some = new SomeComponent($connection, $session, $fileSystem, $filter, $selector);

// ... Or using setters
$some->setConnection($connection);
$some->setSession($session);
$some->setFileSystem($fileSystem);
$some->setFilter($filter);
$some->setSelector($selector);

Подумайте, если нам нужно создать этот объект во многих частях нашего приложения. В будущем, если нам не нужны какие-либо из зависимостей, нам нужно будет пройтись по всему коду, чтобы удалить параметр из любого конструктора или сеттера, где мы инжектировали код. Чтобы решить эту проблему, мы снова возвращаемся к глобальной регистрации для создания компонента. Однако это добавляет новый уровень абстракции перед созданием объекта:

class SomeComponent
{
    // ...

    /**
     * Define a factory method to create SomeComponent instances injecting its dependencies
     */
    public static function factory()
    {
        $connection = new Connection();
        $session    = new Session();
        $fileSystem = new FileSystem();
        $filter     = new Filter();
        $selector   = new Selector();

        return new self($connection, $session, $fileSystem, $filter, $selector);
    }
}

Теперь мы снова оказались в том же месте, где начали, снова создавая зависимости внутри компонента! Мы должны найти решение, которое предотвратит нас от постоянного попадания в плохие практики.

Практичный и элегантный способ решения этих проблем — использование контейнера для зависимостей. Контейнеры действуют как глобальная регистрация, о которой мы говорили ранее. Использование контейнера для зависимостей в качестве моста для получения зависимостей позволяет нам уменьшить сложность нашего компонента:

use Phalcon\Di;
use Phalcon\DiInterface;

class SomeComponent
{
    protected $_di;

    public function __construct(DiInterface $di)
    {
        $this->_di = $di;
    }

    public function someDbTask()
    {
        // Get the connection service
        // Always returns a new connection
        $connection = $this->_di->get("db");
    }

    public function someOtherDbTask()
    {
        // Get a shared connection service,
        // this will return the same connection every time
        $connection = $this->_di->getShared("db");

        // This method also requires an input filtering service
        $filter = $this->_di->get("filter");
    }
}

$di = new Di();

// Register a "db" service in the container
$di->set(
    "db",
    function () {
        return new Connection(
            [
                "host"     => "localhost",
                "username" => "root",
                "password" => "secret",
                "dbname"   => "invo",
            ]
        );
    }
);

// Register a "filter" service in the container
$di->set(
    "filter",
    function () {
        return new Filter();
    }
);

// Register a "session" service in the container
$di->set(
    "session",
    function () {
        return new Session();
    }
);

// Pass the service container as unique parameter
$some = new SomeComponent($di);

$some->someDbTask();

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

© 2011–2017 Phalcon Framework Team
Licensed under the Creative Commons Attribution License 3.0.
https://docs.phalconphp.com/en/latest/reference/di-explained.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API