Скорее всего, в какой-то момент вашего обучения вы столкнулись с термином «внедрение зависимости». Если вы все еще относительно рано в своем обучении, вы, вероятно, сформировали замешательство и пропустили эту часть. Тем не менее, это важный аспект написания поддерживаемого (и тестируемого) кода. В этой статье я объясню это настолько простым способом, насколько я в состоянии.
Пример
Давайте рассмотрим довольно общий кусок кода и обсудим его недостатки.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
class Photo {
/**
* @var PDO The connection to the database
*/
protected $db;
/**
* Construct.
*/
public function __construct()
{
$this->db = DB::getInstance();
}
}
|
На первый взгляд этот код может показаться довольно безопасным. Но учтите тот факт, что мы уже жестко закодировали зависимость: соединение с базой данных. Что если мы хотим ввести другой уровень персистентности? Или подумайте об этом следующим образом: почему объект Photo
должен взаимодействовать с внешними источниками? Разве это не нарушает концепцию разделения интересов? Это конечно делает. Этот объект не должен касаться ничего, что не имеет прямого отношения к Photo
.
Основная идея заключается в том, что ваши классы должны отвечать только за одну вещь. Имея это в виду, он не должен отвечать за подключение к базам данных и другим вещам такого рода.
Давайте восстановим контроль над классом и вместо этого передадим соединение с базой данных. Есть два способа сделать это: инжектор конструктора и метод установки, соответственно. Вот примеры обоих:
Конструктор Инъекция
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
class Photo {
/**
* @var PDO The connection to the database
*/
protected $db;
/**
* Construct.
* @param PDO $db_conn The database connection
*/
public function __construct($dbConn)
{
$this->db = $dbConn;
}
}
$photo = new Photo($dbConn);
|
Выше мы вводим все необходимые зависимости при запуске метода конструктора класса, а не вводим их непосредственно в класс.
Сеттер Инъекция
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
|
class Photo {
/**
* @var PDO The connection to the database
*/
protected $db;
public function __construct() {}
/**
* Sets the database connection
* @param PDO $dbConn The connection to the database.
*/
public function setDB($dbConn)
{
$this->db = $dbConn;
}
}
$photo = new Photo;
$photo->setDB($dbConn);
|
С этим простым изменением класс больше не зависит от какого-либо конкретного соединения. Внешняя система сохраняет полный контроль, как и должно быть. Хотя этот метод может быть не сразу виден, этот метод также значительно облегчает тестирование класса, поскольку теперь мы можем setDB
базу данных при вызове метода setDB
.
Еще лучше, если позже мы решим использовать другую форму персистентности, благодаря внедрению зависимостей, это просто.
«Внедрение зависимостей — это когда компонентам дают свои зависимости через свои конструкторы, методы или непосредственно в поля».
Руб
Есть одна проблема, связанная с использованием инъекции сеттера таким образом: это значительно затрудняет работу с классом. Теперь пользователь должен быть полностью осведомлен о зависимостях класса и должен помнить об их установке соответственно. Подумайте, в конце концов, когда нашему вымышленному классу требуется еще пара зависимостей. Итак, следуя правилам шаблона внедрения зависимостей, мы должны сделать следующее:
1
2
3
4
|
$photo = new Photo;
$photo->setDB($dbConn);
$photo->setConfig($config);
$photo->setResponse($response);
|
Хлоп; класс может быть более модульным, но мы также накопили путаницу и сложность. Раньше пользователь мог просто создать новый экземпляр Photo
, но теперь он должен помнить, чтобы установить все эти зависимости. Вот это боль!
Решение
Решение этой дилеммы состоит в том, чтобы создать контейнерный класс, который будет обрабатывать основную работу за нас. Если вы когда-либо сталкивались с термином «инверсия контроля» (IoC), теперь вы знаете, к чему они относятся.
Этот класс будет хранить реестр всех зависимостей для нашего проекта. Каждый ключ будет иметь связанную лямбда-функцию, которая создает экземпляр ассоциированного класса.
Есть несколько способов справиться с этим. Мы могли бы быть явными и хранить методы, такие как newPhoto
:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
|
// Also frequently called «Container»
class IoC {
/**
* @var PDO The connection to the database
*/
protected $db;
/**
* Create a new instance of Photo and set dependencies.
*/
public static newPhoto()
{
$photo = new Photo;
$photo->setDB(static::$db);
// $photo->setConfig();
// $photo->setResponse();
return $photo;
}
}
$photo = IoC::newPhoto();
|
Теперь $photo
будет равен новому экземпляру класса Photo
со всеми установленными зависимостями. Таким образом, пользователь не должен забывать устанавливать эти зависимости вручную; он просто вызывает метод newPhoto
.
Второй вариант, вместо создания нового метода для каждого экземпляра класса, состоит в том, чтобы написать общий контейнер реестра, например, так:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
class IoC {
/**
* @var PDO The connection to the database
*/
protected static $registry = array();
/**
* Add a new resolver to the registry array.
* @param string $name The id
* @param object $resolve Closure that creates instance
* @return void
*/
public static function register($name, Closure $resolve)
{
static::$registry[$name] = $resolve;
}
/**
* Create the instance
* @param string $name The id
* @return mixed
*/
public static function resolve($name)
{
if ( static::registered($name) )
{
$name = static::$registry[$name];
return $name();
}
throw new Exception(‘Nothing registered with that name, fool.’);
}
/**
* Determine whether the id is registered
* @param string $name The id
* @return bool Whether to id exists or not
*/
public static function registered($name)
{
return array_key_exists($name, static::$registry);
}
}
|
Не позволяйте этому коду напугать вас; это действительно очень просто. Когда пользователь вызывает метод IoC::register
, он добавляет идентификатор, такой как photo
, и связанный с ним преобразователь, который является просто лямбда-выражением, которое создает экземпляр и устанавливает любые необходимые зависимости от класса.
Теперь мы можем регистрировать и разрешать зависимости через класс IoC
, например так:
01
02
03
04
05
06
07
08
09
10
11
|
// Add `photo` to the registry array, along with a resolver
IoC::register(‘photo’, function() {
$photo = new Photo;
$photo->setDB(‘…’);
$photo->setConfig(‘…’);
return $photo;
});
// Fetch new photo instance with dependencies set
$photo = IoC::resolve(‘photo’);
|
Итак, мы можем наблюдать, что с этим шаблоном мы не создаем экземпляр класса вручную. Вместо этого мы регистрируем его в контейнере IoC
, а затем запрашиваем конкретный экземпляр. Это уменьшает сложность, которую мы представили, когда мы удалили жестко закодированные зависимости из класса.
1
2
3
4
5
|
// Before
$photo = new Photo;
// After
$photo = IoC::resolve(‘photo’);
|
Практически одинаковое количество символов, но теперь класс значительно более гибкий и тестируемый. В реальных условиях вы, вероятно, захотите расширить этот класс, чтобы учесть и создание синглетонов.
Методы волшебства
Если мы хотим еще больше сократить длину класса IoC
, мы можем воспользоваться магическими методами, а именно __set()
и __get()
, которые будут срабатывать, если пользователь вызывает метод, который не существует в классе.
01
02
03
04
05
06
07
08
09
10
11
12
13
|
class IoC {
protected $registry = array();
public function __set($name, $resolver)
{
$this->registry[$name] = $resolver;
}
public function __get($name)
{
return $this->registry[$name]();
}
}
|
Популяризованный Фабьеном Потенциером, это супер-минимальная реализация, но она будет работать. __get()
ли __get()
или set()
или нет, зависит от того, устанавливает ли пользователь значение или нет.
Основное использование будет:
01
02
03
04
05
06
07
08
09
10
11
|
$c = new IoC;
$c->mailer = function() {
$m = new Mailer;
// create new instance of mailer
// set creds, etc.
return $m;
};
// Fetch, boy
$mailer = $c->mailer;
|
Спасибо за прочтение!