Подтвердить что ты не робот

Инъекция зависимостей: вытягивание необходимых компонентов, когда они действительно необходимы

Суть за DI заключается в том, чтобы освободить класс от создания и подготовки объектов, от которых он зависит, и подталкивать их. Это звучит очень разумно, но иногда классу не нужны все объекты, которые вставляются в него для выполнения его функции. Причиной этого является "раннее возвращение", которое происходит при некорректном вводе пользователя или исключении, вызванном одним из требуемых объектов раньше, или недоступности определенного значения, необходимого для создания экземпляра объекта до тех пор, пока не будет выполняться блок кода.

Более практические примеры:

  • вводить объект подключения к базе данных, который никогда не будет использоваться, поскольку пользовательские данные не проходят проверку (при условии, что для проверки этих данных не используются триггеры).
  • вводящие excel-подобные объекты (PHPExcel, например), которые собирают входные данные (тяжелые для загрузки и создания экземпляра, потому что вся библиотека втягивается и никогда не используется, поскольку проверка делает исключение раньше, чем происходит запись)
  • значение переменной, определенное в классе, но не инжектор во время выполнения; например, компонент маршрутизации, который определяет класс и метод контроллера (или команды), который должен вызываться на основе пользовательского ввода
  • хотя это может быть проблема проектирования, но существенный класс обслуживания, который зависит от множества компонентов, но использует только 1/3 из них для каждого запроса (причина, почему я склонен использовать командные классы вместо контроллеры)

Таким образом, толкание всех необходимых компонентов противоречит "ленивой загрузке" таким образом, что некоторые компоненты создаются и никогда не используются, что немного непрактично и влияет на производительность. Что касается PHP, то больше загружаются файлы, анализируются и компилируются. Это особенно болезненно, если объекты, в которые вставляются, имеют свои собственные зависимости.

я вижу 3 пути вокруг него, 2 из которых звучат не очень хорошо:

  • вводя factory
  • инъекция инжектора (анти-шаблон)
  • впрыскивание некоторой внешней функции, вызываемой изнутри класса, как только будет достигнута соответствующая точка (smtg like "retrieve a Экземпляр PHPExcel после завершения проверки данных "), это то, что я склонны использовать из-за своей гибкости.

Вопрос в том, что лучший способ справиться с такими ситуациями/что вы, ребята, используете?

UPDATE: @GordonM вот примеры из трех подходов:

//inject factory example
interface IFactory{
    function factory();
}
class Bartender{
    protected $_factory;

    public function __construct(IFactory $f){
        $this->_factory = $f;
    }
    public function order($data){
        //validating $data
        //... return or throw exception
        //validation passed, order must be saved
        $db = $this->_factory->factory(); //! factory instance * num necessary components
        $db->insert('orders', $data);
        //...
    }
}

/*
inject provider example
assuming that the provider prepares necessary objects
(i.e. injects their dependencies as well)
*/
interface IProvider{
    function get($uid);
}
class Router{
    protected $_provider;

    public function __construct(IProvider $p){
        $this->_provider = $p;
    }
    public function route($str){
        //... match $str against routes to resolve class and method
        $inst = $this->_provider->get($class);
        //...
    }
}

//inject callback (old fashion way)
class MyProvider{
    protected $_db;
    public function getDb(){
        $this->_db = $this->_db ? $this->_db : new mysqli();
        return $this->_db;
    }
}
class Bartender{
    protected $_db;

    public function __construct(array $callback){
        $this->_db = $callback;
    }
    public function order($data){
        //validating $data
        //... return or throw exception
        //validation passed, order must be saved
        $db = call_user_func_array($this->_db, array());
        $db->insert('orders', $data);
        //...
    }
}
//the way it works under the hood:
$provider = new MyProvider();
$db = array($provider, 'getDb');
new Bartender($db);

//inject callback (the PHP 5.3 way)
class Bartender{
    protected $_db;

    public function __construct(Closure $callback){
        $this->_db = $callback;
    }
    public function order($data){
        //validating $data
        //... return or throw exception
        //validation passed, order must be saved
        $db = call_user_func_array($this->_db, array());
        $db->insert('orders', $data);
        //...
    }
}
//the way it works under the hood:
static $conn = null;
$db = function() use ($conn){
    $conn = $conn ? $conn : new mysqli();
    return $conn;
};
new Bartender($db);
4b9b3361

Ответ 1

Я много думал об этой проблеме в последнее время в планировании крупного проекта, который я хочу делать так же правильно, как и по-человечески (придерживайтесь LoD, не зависимостей от жесткого кодирования и т.д.). Моей первой мыслью был подход "Inject a factory", но я не уверен, что путь. В "Чистом кодексе" от Google говорится, что если вы дойдете до объекта, чтобы получить тот объект, который вам действительно нужен, вы нарушите LoD. Это, по-видимому, исключает идею впрыска factory, потому что вам нужно достичь через factory, чтобы получить то, что вы действительно хотите. Может быть, я пропустил какой-то момент, который делает все в порядке, но пока я точно не знаю, что я обдумываю другие подходы.

Как вы делаете инъекцию функции? Я бы предположил, что вы передаете обратный вызов, который создает экземпляр объекта, который вы хотите, но пример кода будет приятным.

Если вы могли бы обновить свой вопрос с примерами кода, как вы делаете три упомянутых вами стили, это может быть полезно. Я особенно хочу увидеть "инъекцию инжектора", даже если это антипаттерн.

Ответ 2

Одна из идей, которая действительно возникла, - это прокси-объект. Он реализует один и тот же интерфейс как фактический объект, который вы хотите передать, но вместо того, чтобы внедрять что-либо, он просто хранит экземпляр метода реального класса и переадресации на него.

interface MyInterface 
{
    public function doFoo ();
    public function isFoo ();
    // etc
}

class RealClass implements MyInterface
{
    public function doFoo ()
    {
         return ('Foo!');
    }

    public function isFoo ()
    {
        return ($this -> doFoo () == 'Foo!'? true: false);
    }

    // etc
}

class RealClassProxy implements MyInterface
{
    private $instance = NULL;

    /**
     * Do lazy instantiation of the real class
     *
     * @return RealClass
     */
    private function getRealClass ()
    {
        if ($this -> instance === NULL)
        {
            $this -> instance = new RealClass ();
        }
        return $this -> instance;
    }

    public function doFoo ()
    {
        return $this -> getRealClass () -> doFoo ();
    }

    public function isFoo ()
    {
        return $this -> getRealClass () -> isFoo ();
    }

    // etc
}

Поскольку прокси имеет тот же интерфейс, что и реальный класс, вы можете передать его как аргумент любой функции/методу, который вводит подсказки для интерфейса. Принцип замещения Лискова выполняется для прокси-сервера, поскольку он отвечает на все те же сообщения, что и у реального класса, и возвращает те же результаты (интерфейс обеспечивает это, по крайней мере, для значений метода). Тем не менее, реальный класс не получает экземпляр, если сообщение фактически не отправляется в прокси, что делает ленивое создание реального класса за кулисами.

function sendMessageToRealClass (MyInterface $instance)
{
    $instance -> doFoo ();
}

sendMessageToRealClass (new RealClass ());
sendMessageToRealClass (new RealClassProxy ());

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

EDIT: я изначально написал этот ответ с идеей подкласса реального объекта, чтобы вы могли использовать этот метод с объектами, которые не реализуют никаких интерфейсов, таких как PDO. Я изначально считал, что интерфейсы являются правильным способом для этого, но мне нужен подход, который не полагался на класс, привязанный к интерфейсу. По размышлению это была большая ошибка, поэтому я обновил ответ, чтобы отразить то, что я должен был сделать в первую очередь. Однако эта версия означает, что вы не можете напрямую применить этот метод к классам без связанного интерфейса. Вам придется обернуть такие классы в другой класс, который обеспечивает интерфейс для прокси-подхода, чтобы быть жизнеспособным, что означает еще один слой косвенности.

Ответ 3

Если вы хотите реализовать ленивую загрузку, у вас есть два способа сделать это (как вы уже писали в теме):

  • вместо того, чтобы вводить экземпляр объекта, который вам нужен может, вы вводите Factory или Builder. Разница между ними заключается в том, что экземпляр Builder создан для возврата одного типа объекта (возможно, с разными настройками), тогда как Factory создает разные типы экземпляров (с тем же временем жизни и/или реализующим тот же интерфейс).

  • используйте анонимную функцию, которая вернет вам экземпляр. Это будет выглядеть примерно так:

    $provider = function() {
        return new \PDO('sqlite::memory:');
    };
    

    Только когда вы вызываете эту анонимную функцию, создается экземпляр PDO и устанавливается соединение с базой данных.

То, что я обычно делаю в своем коде, сочетает в себе. Вы можете оснастить Factory таким provider. Это, например, позволяет вам иметь одно соединение для всех объектов, созданных указанным factory, и соединение создается только при первом запросе экземпляра из Factory.

Другой способ комбинировать оба метода (который я еще не использовал) должен был бы создать класс полного удара provider, который в конструкторе принимает анонимную функцию. Затем factory может проходить вокруг этого же экземпляра provider, а дорогостоящий объект (PHPExcel, Doctrine, SwiftMailer или какой-либо другой экземпляр) создается только после того, как Product из этого Factory в первый раз обращается к provider (не может найти лучшего имени для описания всех объектов, созданных тем же factory), и запрашивает его. После этого этот дорогой объект распределяется между всеми Products Factory.

... мои 2 цента

Ответ 4

Я выбрал lazy-injection (т.е. вводя класс Proxy):

class Class1 {
    /**
     * @Inject(lazy=true)
     * @var Class2
     */
    private $class2;

    public function doSomething() {
        // The dependency is loaded NOW
        return $this->class2->getSomethingElse();
    }

Здесь зависимость (класс2) не вводится напрямую: вводится прокси-класс. Только когда используется прокси-класс, загружаемый зависимость.

Это возможно в PHP-DI (инфраструктура инъекции зависимостей).

Отказ от ответственности: я работаю в этом проекте