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

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

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

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

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

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

  • вводят завод
  • впрыскивание инжектора (анти-шаблон)
  • вводят некоторую внешнюю функцию, которая вызывается изнутри класса после достижения соответствующей точки (smtg like «возвращает экземпляр PHPExcel после завершения проверки данных»); это то, что я склонен использовать из-за его гибкости

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

ОБНОВЛЕНИЕ : @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 (ie 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); 

Я много думал об этой проблеме в последнее время при планировании крупного проекта, который я хочу делать так же правильно, как и по-человечески (придерживайтесь LoD, не зависимостей от жесткого кодирования и т. Д.). Моей первой мыслью был подход «Ввести завод», но я не уверен, что это путь. В «Чистом кодексе» от Google говорится, что если вы дойдете до объекта, чтобы получить тот объект, который вам действительно нужен, вы нарушите LoD. Казалось бы, это исключает идею инъекции фабрики, потому что вам нужно добраться до завода, чтобы получить то, что вы действительно хотите. Может быть, я пропустил какой-то момент, который делает все в порядке, но пока я точно не знаю, что я обдумываю другие подходы.

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

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

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

 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. Я изначально считал, что интерфейсы являются правильным способом для этого, но мне нужен подход, который не полагался на класс, привязанный к интерфейсу. По размышлению это была большая ошибка, поэтому я обновил ответ, чтобы отразить то, что я должен был сделать в первую очередь. Однако эта версия означает, что вы не можете напрямую применить этот метод к классам без связанного интерфейса. Вам придется обернуть такие классы в другой класс, который обеспечивает интерфейс для прокси-подхода, чтобы быть жизнеспособным, что означает еще один слой косвенности.

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

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

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

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

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

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

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

… мои 2 цента

Я выбрал ленивую инъекцию (т.е. инъекцию класса 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 (инфраструктура инъекции зависимостей).

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