Статьи

Инъекция зависимости: А?

Скорее всего, в какой-то момент вашего обучения вы столкнулись с термином «внедрение зависимости». Если вы все еще относительно рано в своем обучении, вы, вероятно, сформировали замешательство и пропустили эту часть. Тем не менее, это важный аспект написания поддерживаемого (и тестируемого) кода. В этой статье я объясню это настолько простым способом, насколько я в состоянии.


Давайте рассмотрим довольно общий кусок кода и обсудим его недостатки.

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), теперь вы знаете, к чему они относятся.

Определение : В программной инженерии Inversion of Control (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;

Спасибо за прочтение!