Объяснение инъекции зависимостей
Следующий пример немного длинный, но он пытается объяснить, почему 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