Dependency Injection, DIC и Service Locator

Дмитрий Науменко

Yii core team, HiQDev

Темы на сегодня:

  • Зависимости
  • Dependency Injection (DI)
  • Dependency Injection Container (DIC)
  • Service Locator
  • Использование DI в Yii

Будет очень много кода

Зависимости

Класс A зависит от класса B

Просто, как двери

Двери с электронным замком и кнопка открытия

  • Кнопка обращается к приложению по HTTP
  • Открываем замок, если это разрешено
  • Выводим сообщение на дисплей

Ща запилим, будет торт!

public function actionOpen() {
    $result = false;
    $canBeOpened = HourSettings::find()->select('can_be_opened')
                        ->where(['hour' => date('H')])->scalar();

    if ($canBeOpened) {
        $fd = dio_open('/dev/ttyS0', O_RDWR | O_NOCTTY | O_NONBLOCK);
        dio_fcntl($fd, F_SETFL, O_SYNC);
        dio_tcsetattr($fd, ['baud' => 9600, 'bits' => 8]);
        dio_write($fd, "OPEN\n");
        $result = dio_read($fd, 3) === "1\n";
        dio_close($fd);
    }

    return $this->render('open', ['result' => $result]);
}
  • данные
  • логика предметной области
  • бизнес-логика
  • конфигурация
  • представление

Проблемы:

  • Всё перемешано
  • Нет архитектуры
  • Тестировать крайне сложно
  • В будущем – копипаста, баги, боль

Будем трансформировать!

Разделяем

class DoorManager
{
    public function open()
    {
        $door = new Door();
        return $door->open();
    }
}
public function actionOpen()
{
    $doorManager = new DoorManager();
    return $doorManager->open();
}

actionOpen() зависит от DoorManager

DoorManager зависит от Door

Связи:

  • High Cohesion
  • Low Coupling

Кнопка — неактуально!

Выдать всем RFID и сделать логирование!

class DoorManager
{
    public function open(RfidKey $rfid)
    {
        $door = new Door();
        $log = new EmailAccessLogger('alert@superdoors.com');
        $auth = new RfidAuthenticator($rfid, [
            'users' => User::find()->where(['active' => 1])->all()
        ]);

        if ($auth->canOpenDoor($door)) {
            $log->accessGranted($door, $rfid);
            return $door->open();
        } else {
            $log->accessDenied($door, $rfid);
            return false;
        }
    }
}

Создано множество зависимостей

Win:
  • задача решена
Lose:
  • метод open() божественный
  • порождён High Coupling
  • смешан сбор данных, бизнес-логика и конфигурация
  • убита возможность тестировать

Ах да, двери нужно еще уметь принудительно закрывать...

Бизнес всегда порождает боль новые требования



Плохой код порождает боль

Эволюция управления зависимостями

Инициализация в конструкторах

class DoorManager {
    private $door; // key, auth, log

    public function __construct(RfidKey $key) {
        $this->key = $key;
        $this->door = new Door();
        $this->log = new EmailAccessLogger('alert@superdoors.com');
        $this->auth = new RfidAuthenticator($this->key, [
            'users' => User::find()->where(['active' => 1])->all()
        ]);
    }

    public function open() {
        if ($this->auth->canOpenDoor($this->door)) {
            $this->log->accessGranted($this->door, $this->rfid);
            $this->door->open();
        } else {
            $this->log->accessDenied($this->door, $this->rfid);
        }
    }

    public function close() { } // implementation
}
Win:
  • задача решена
  • разгрузили божественный метод
  • метод close() не будет копи-пастой open()
Lose:
  • божественный конструктор класса
  • все еще смешан сбор данных, бизнес-логика и конфигурация
  • тот же High Coupling
  • возможности тестировать нет

Часть дверей переводится на магнитные замки.

Протокол управления отличается.

Возможные решения:

  • добавить if внутри объекта Door()
  • добавить if перед созданием объекта Door()
  • разделить проект на две ветки: master и magnet-door-master
  • написать MagnetDoorManager

Нужна инверсия управления!

Inversion of Control — принцип объектно-ориентированного программирования, используемый для уменьшения связанности в компьютерных программах

Идеи:

  • Модули верхнего уровня не зависят от модулей нижнего уровня — и те, и другие зависят от абстракций
  • Абстракции не зависят от деталей — детали зависят от абстракций

Создаём интерфейсы

interface DoorInterface {
    public function open();
    public function close();
}

class Door implements DoorInterface {
    public function open() { } // implementation
    public function close() { } // implementation
}

class MagnetDoor implements DoorInterface {
    public function open() { } // implementation
    public function close() { } // implementation
}

class FakeDoor implements DoorInterface { // for testing purpose
    public function open() { return true; }
    public function close() { return true; }
}

Делаем фабрики

abstract class AbstractDoorFactory
{
   /** @return DoorInterface */
   abstract public function build();
}

class DoorFactory extends AbstractDoorFactory
{
   public function build() {
      return new Door();
   }
}

class MagnetDoorFactory extends AbstractDoorFactory
{
   public function build() {
      return new MagnetDoor();
   }
}

Изменяем конструктор

class DoorManager {
    private $door; // key, auth, log

    public function __construct(DoorInterface $door, RfidKey $key) {
        $this->door = $door;
        $this->key = $key;
        $this->log = new EmailAccessLogger('alert@superdoors.com');
        $this->auth = new RfidAuthenticator($this->key, [
            'users' => User::find()->where(['active' => 1])->all()
        ]);
    }
}
public function actionOpen($keySecret)
{
    $key = new RfidKey($keySecret);
    $door = (new MagnetDoorFactory())->build();

    $doorManager = new DoorManager($door, $key);
    return $doorManager->open();
}
Win:
  • задача решена
  • конструктор теперь не создаёт объект Door
  • код стал независим от реализации DoorInterface
Lose:
  • появилась зависимость на фабрику
  • конструктор всё еще божественный
  • всё еще смешаны сбор данных, бизнес-логика и конфигурация
  • усложнилось создание DoorManager

Dependency Injection — это набор практик, который помогает строить приложения с низким уровнем связанности между компонентами.

это НЕ только:
  • паттерн
  • фича фреймворка
  • библиотека
это:
  • общие рекомендации
  • способ построения архитектуры кода
  • способ мышления

Варианты DI

DI через сеттеры

class DoorManager {
    private $door;
    private $key;
    private $auth;
    private $log;

    public function __construct(DoorInterface $door) { }

    public function setKey(KeyInterface $key) { }
    public function setAuth(AuthManagerInterface $auth) { }
    public function setLog(LoggerInterface $log) { }
}
public function actionOpen($keySecret)
{
    $door = $this->getDoor();
    $doorManager = new DoorManager($door);
    $doorManager->setKey(new RfidKey($keySecret));
    // ...
    return $doorManager->open();
}
Win:
  • нет божественных методов
  • DoorManager стал независим от реализаций
  • все зависимости – опциональны
  • стало реальным тестирование
Lose:
  • сильно усложнилась инициализация DoorManager
  • все зависимости – опциональны

Все зависимости – опциональны

public function open()
{
    if ($this->log !== null) {
        $this->log->accessGranted($this->door, $this->key);
    }
}

Опциональные зависимости не нужны

class FakeLogger implements LoggerInterface
{
    public function accessGranted(DoorInterface $door, KeyInterface $key)
    {
        return true;
    }
}

DI через конструктор

class DoorManager {
    private $door; // key, auth, log

    public function __construct(
        KeyInterface $key,              DoorInterface $door,
        AuthManagerInterface $auth,     LoggerInterface $log,
    ) {
        $this->door = $door; // key, auth, log
    }
}
Win:
  • все зависимости обязательны
  • убрали сеттеры, которые могут дать неопределенное состояние
Lose:
  • инициализация DoorManager всё еще сложная

Как мы сейчас создаём зависимости?

public function actionOpen($keySecret)
{
    $key = new RfidKey($keySecret);
    $door = new MagnetDoor('some-door-id');

    $users = User::find()->active()->all();
    $auth = new RfidAuthenticator($key, $users);
    $log = new EmailAccessLogger('alert@superdoors.com');

    $doorManager = new DoorManager($key, $door, $auth, $log);
    return $doorManager->open();
}
  • Создание зависимостей за пределами класса
  • Сделали новый божественный метод - actionOpen
  • Теперь повторить то же самое в actionClose?...

Похоже на путь в никуда

DiC!

Dependency injection Container

Dependency Injection Container — класс, управляющий созданием объектов других классов.

Наш actionOpen()

public function actionOpen($keySecret)
{
    $key = new RfidKey($keySecret);
    $door = new MagnetDoor('some-door-id');

    $users = User::find()->active()->all();
    $auth = new RfidAuthenticator($key, $users);
    $log = new EmailAccessLogger('alert@superdoors.com');

    $doorManager = new DoorManager($key, $door, $auth, $log);
    return $doorManager->open();
}

DIC на коленке

class Container {
    /** @return KeyInterface */
    public function getKey($keySecret) {
        return new RfidKey($keySecret);
    }
    /** @return AuthManagerInterface */
    public function getAuthenticator($key, $users) {
        return new RfidAuthenticator($key, $users);
    }
    /** @return LoggerInterface */
    public function getLogger() {
        return new EmailAccessLogger('alert@superdoors.com');
    }
    /** @return DoorInterface */
    public function getDoor() {
        return new MagnetDoor('some-door-id');
    }
    /** @return DoorManagerInterface */
    public function getDoorManager($keySecret, $users) {
        $key = $this->getKey($keySecret);
        $door = $this->getDoor();
        $auth = $this->getAuthenticator($key, $users);
        $logger = $this->getLogger();

        return new DoorManager($key, $door, $auth, $logger);
    }
}
public function actionOpen($keySecret)
{
    $container = new Container();

    $users = User::find()->active()->all();
    $doorManager = $container->getDoorManager($keySecret, $users);

    return $doorManager->open();
}
Win:
  • контроллер без явных зависимостей на наши классы
  • есть единая точка разрешения зависимостей
  • легко тестировать
Lose:
  • слишком императивное описание

Зачем мы каждый раз создаём объекты, которые не изменятся в контексте запроса?...

Контейнер может хранить объект после создания:

class Container {
    private $logger;

    /** @return LoggerInterface */
    public function getLogger()
    {
        if (!isset($this->logger)) {
            $this->logger = new EmailAccessLogger('alert@superdoors.com')
        }
        return $this->logger;
    }
}
}

Объект может быть сохранён после создания, если:

  • Он может быть предварительно сконфигурирован
  • Он актуален в течение всего runtime
  • Хочется сделать singleton

Классические примеры:

  • Logger
  • Mailer
  • Cache
  • Session
  • DB

Service Locator

  • Содержит описание компонентов (сервисов) приложения
  • Хранит созданные объекты компонентов
  • Не занимается удовлетворением зависимостей классов
  • Может использоваться вместе с DI

Используя Service Locator, мы явно запрашиваем зависимости и зависимы от самого локатора.


Используя DiC, что-то даёт зависимости в конструктор и нам всё равно, что это.

Автоматическая инъекция и рефлексия

function getDependencies($class)
{
    $dependencies = [];
    $reflection = new ReflectionClass($class);

    $constructor = $reflection->getConstructor();
    if ($constructor !== null) {
        foreach ($constructor->getParameters() as $param) {
            $c = $param->getClass();
            if ($c instanceof \ReflectionClass) {
                $dependencies[] = $c->getName();
            }
        }
    }

    return $dependencies;
}

Разные реализации DI:

Допилим двери?

Конфигурируем DiC

$c = Yii::$container;

// interface to concrete implementation
$c->set(KeyInterface::class, RfidKey::class);
$c->set(AuthManagerInterface::class, RfidAuthenticator::class);

// interface to component from Service Locator
$c->set(LoggerInterface::class, function () {
    return Yii::$app->get('log');
});
$c->set(DoorInterface::class, function () {
    return Yii::$app->get('door');
});

// interface to concrete implementation - complex initialisation
$c->set(DoorManagerInterface::class, function ($cont, $params, $config) {
    // DoorManager constructor params order:
    // 0 - Key, 1 - Door, 2 - Auth, 3 - Logger
    $params[2] = $cont->get(AuthManagerInterface::class, [$params[0]]);

    return $cont->get(DoorManager::class, $params, $config);
});
public function actionOpen($keySecret)
{
    $container = $this->container;

    $key = $container->get(KeyInterface::class, [$keySecret]);
    $doorManager = $container->get(DoorManagerInterface::class, [$key]);

    return $doorManager->open();
}
Win:
  • красивый код
  • нет божественных методов
  • Low Coupling
  • взаимодействие по интерфейсам
  • нет new Something() → тестируемо в изоляции
Lose:
  • чуть сложновато :)
  • на уровне контроллера появилась зависимость на

Где не стоит использовать DiC?

  • Внутри модуля
  • В коде, который не должен зависить от самого контейнера

Общие рекомендации:

Ожидайте интерфейсы, а не реализации

Не храните в DiC данные

Не бойтесь создавать много маленьких классов

Не увлекайтесь абстракциями. Плата за гибкость — сложность

Развивайтесь, делитесь опытом и делайте крутые вещи!

Вопросы?