Dependency Injection

Избавляемся от излишней связности

Dependency Injection

Избавляемся от запутанных зависимостей

Александр Паньшин

О чем поговорим?

Я хочу рассказать про следующие вещи:

Внезапно, начнем с примера!

Предположим, в нашем веб-приложении надо хранить состояние пользователя:

Интерфейс

interface UserInterface {
    public function setLocale($locale);
    public function getLocale();
    public function isAuthenticated();
    public function getId();
    public function hasCredential($credential);
}

Мерзкая природа HTTP

HTTP по своей природе не поддерживает сохранения состояния между запросами.

Именно поэтому в PHP есть сессии.

        
class SessionStorage {
    public function __construct($cookie_name = 'PHPSESSID')
    {
        session_name($cookie_name);
        session_start();
    }

    public function get($key)
    {
        return $_SESSION[$key];
    }

    public function set($key, $value)
    {
        $_SESSION[$key] = $value;
    }
}
        
    

class User {
    protected $storage;

    public function __construct() {
        $this->storage = new SessionStorage();
    }

    public function setLocale($locale) {
        $this->storage->set('locale', $locale);
    }

    public function getLocale() {
        return $this->storage->get('locale');
    }
}

$user = new User();
    

А как поменять имя куки?

Только хардкод! Только хардкор!

class User {
    protected $storage;

    public function __construct() {
        $this->storage = new SessionStorage('AWESOME_COOKIE_NAME');
    }
}

Константочку запилим:)

define('SESSION_COOKIE_NAME', 'AWESOME_COOKIE_NAME');
class User {
    protected $storage;

    public function __construct() {
        $this->storage = new SessionStorage(SESSION_COOKIE_NAME);
    }
}

Пользователь передаст

class User {
    protected $storage;

    public function __construct($cookieName) {
        $this->storage = new SessionStorage($cookieName);
    }
}

А где мы храним сессии?

Запилим Registry God Object?

class User {
    protected $storage;

    public function __construct() {
        $this->storage = Registry::get('session');
    }
}

Ой!

Теперь User зависит от Registry и скрытно — от SessionStorage.

Хотели как лучше...

Простое и очевидное решение

class User {
    protected $storage;
    public function __construct(SessionStorage $storage) {
        $this->storage = $storage;
    }
}
$user = new User(new MySQLSessionStorage('SESSION_ID'));

Это и есть Dependency Injection

Мы не создаем зависимости внутри нашего кода, мы их внедряем извне.

Constructor Injection

class A {
    public function __construct(B $b);
}

Используем для обязательных зависимостией.

Setter Injection

class A {
    public function setB(B $b);
}

Используем для необязательных зависимостией.

Property Injection

class A {
    /** @var B */
    public $b;
}

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

Теперь примерчик посложнее

class Controller {
    protected $templating;
    protected $routing;
    protected $user;

    public function whateverAction(Request $request);
}

Things're getting worse

Как быть?

Dependency Injection Container

То самое третье лицо, ага.

Раз уж мы собираемся на Laravel...

Поехали!

composer require "illuminate/container ~5.0"

Патчим Кохану

use Illuminate\Container\Container;
class Kohana extends Kohana_Core {
    public static function get_container() {
        return Container::getInstance();
    }
}

Нам нужен конфиг!

bootstrap.php:

// Tons of bootstrap code
require_one __DIR__.'/config/container.php';

/application/config/container.php

use Illuminate\Container\Container;
$container = new Container();
// All the services definitions here...
Container::setInstance($container);

Самое простое определение

$container->bind('SessionStorage', 'RedisSessionStorage');
$container->bind('UserInterface', 'User');
$user = $container->make('UserInterface');
// или $container['UserInterface'];

Если другому сервису будет нужен инстанс SessionStorage, контейнер создаст объект RedisSessionStorage и передаст его.

Ручное управление

$container->bind('encryptor', function() {
    $rsa = new \phpseclib\Crypt\RSA();
    $rsa->setSignatureMode(\phpseclib\Crypt\RSA::ENCRYPTION_PKCS1);
    $rsa->loadKey(Kohana::$config->load('google_play')->get('key'));
    return $rsa;
});

Singleton

$container->singleton('FooBar', function(Container $c) {
    return new FooBar();
});

Передаем параметры для сборки:

$container->bind('alias', function(Container $c, $parameters) {
    return new MyAwesomeClass($parameters);
});
$object = $container->make('alias', ['name' => 'value']);

Попробуем собрать контроллер?



class Controller_API_Purchases extends Controller_API {
    /** @var  RSA */
    private $encryptor;

    /** @var Service_SubscriptionManager */
    private $manager;

    public function __construct(Request $request, Response $response,
        RSA $encryptor, Service_SubscriptionManager $subscriptionManager) {

        parent::__construct($request, $response);
        $this->encryptor = $encryptor;
        $this->manager = $subscriptionManager;
    }
}

Лирическое отступление

Честно говоря, меня прям бесит, что Request и Response создаются до контроллера. По идее, каждый action контроллера должен принимать Request и создавать Response:)

Но разработчиков Kohana мое мнение волнует мало:)

Создаем сервис:

$container->bind('subscription_manager', 'Service_SubscriptionManager');
$container->bind('controller.purchases', function($c, $parameters) {
    return new Controller_API_Purchases(
        $parameters['request'],
        $parameters['response'],
        $c['encryptor'],
        $c['subscription_manager']);
});

Кажется, нам надо пропатчить Кохану еще раз

Ну да, она пока не умеет создавать контроллеры из сервисов.

Request_Client_Internal

public function execute_request(Request $request, Response $response)
{
    // tons of code here
    $controller = $this->create_controller(
        $prefix, $controller, $request, $response
    );
    $response = $controller->execute();
}
private function create_controller($prefix, $controller_name, Request $request, Response $response)
    $container = Kohana::get_container();
    $service = 'controller.'.strtolower($controller_name);
    if ($container->bound($service)) {
        $controller = $container->make($service, ['request' => $request, 'response' => $response]);
    }
    else {
        // Создаем контроллер стандартным методом Коханы,
        // который мы извлекли из execute_request();
        // Create a new instance of the controller
        $controller =  $class->newInstance($request, $response);
    }
    if ($controller instanceof Service_ContainerAware) {
        $controller->set_container($container);
    }
    return $controller;
}

Что еще за Service_ContainerAware?

Это способ внедрить сам контейнер в качестве зависимости.

Используйте это в случае крайней необходимости!

Сервис, зависимый от контейнера

class Controller_Awesome extents Controller
            implements Service_ContainerAware {
    /** @var Container */
    private $container;
    public function set_container(Container $container) {
        $this->container = $container;
    }
}

Соглашение об именовании сервисов

Route::set('tour_finish', 'tour/finish')->filter(['Filters', 'auth'])->defaults([
            'controller' => 'tour', 'action' => 'finish']);
$container->bind('controller.tour', function($c, $parameters) {
    return new Controller_Tour($parameters['request'],
            $parameters['response']);
});

Анти-паттерны

Control-Freak

class User {
    protected $storage;
    public function __construct() {
        $this->storage = new SessionStorage();
    }

Этот парень слишком многое знает.

Превращение DIC в ServiceLocator

public function set_container(Container $container) {
    $this->container = $container;
}

Первое правило DIC: никто не должен знать о DIC.

by the way, ServiceLocator — не плохо. Плохо смешивать!

Чтиво на ночь: