Зависимость переопределения вместо инъекции зависимостей?

Я напишу длинный вопрос с короткой версией вопроса:

Короткий вариант вопроса

Что не так, что позволяет объекту создавать свои собственные зависимости, а затем предоставлять аргументы конструктора (или методы setter) для просто переопределения экземпляров по умолчанию?

class House
{
   protected $door;
   protected $window;
   protected $roof;

   public function __construct(IDoor $door = null, IWindow $window = null, IRoof $roof = null)
   {
      $this->door   = ($door)   ? $door   : new Door;
      $this->window = ($window) ? $window : new Window;
      $this->roof   = ($roof)   ? $roof   : new Roof;
   }
}

Длинная версия вопроса

Моя мотивация для этого вопроса заключается в том, что инъекция зависимостей требует, чтобы вы прыгали через обручи, чтобы дать объекту то, что ему нужно. Контейнеры IoC, заводы, локаторы сервисов... все это приводит к множеству дополнительных классов и абстракций, которые усложняют API вашего приложения, и я бы сказал, что во многих случаях тестирование так же сложно.

Не логично ли, что объект действительно знает, какие зависимости ему нужны, чтобы нормально функционировать?

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

Между тем, если вам нужно добавить класс House к вашему приложению, ТОЛЬКО нужно закодировать класс House, а не factory и/или контейнер DI поверх него. Кроме того, любой клиентский код, который использует дом, может просто включать дом, и ему не нужно давать дом factory или абстрактный сервисный локатор где-то сверху. Все становится чрезвычайно прямым, без кода посредника, и создается только тогда, когда это необходимо.

Неужели я совершенно не согласен с тем, что если у объекта есть зависимости, он должен иметь возможность загружать их самостоятельно, предоставляя механизм для перегрузки этих зависимостей при желании?

Пример

#index.php (front controller)

$db = new PDO(...);
$cache = new Cache($dbGateway);
$session = new Session($dbGateway);
$router = new Router;

$router::route('/some/route', function() use ($db, $cache, $session) 
{   
   $controller = new SomeController($db, $cache, $session);
   $controller->doSomeAction();
});



#SomeController.php

class SomeController
{
   protected $db;
   protected $cache;
   protected $session;

   public function __construct(PDO $db, ICache $cache, ISession $session)
   {
      $this->db = $db;
      $this->cache = $cache;
      $this->session = $session;
   }

   public function doSomeAction()
   {
      $user = new \Domain\User;
      $userData = new \Data\User($this->db);

      $user->setName('Derp');
      $userData->save($user);
   }
}

Теперь, в очень большом приложении со многими различными классами/классами данных и контроллерами, я чувствую, что мне нужно передать объект БД ЧЕРЕЗ каждый контроллер (который ему не понадобится), чтобы передать его каждому преобразователю данных ( который ему понадобится), немного вонючий.

И по расширению, передавая локатор сервисов или контейнер DI через контроллер, просто для того, чтобы найти базу данных, а затем передать ее в datamapper каждый раз, также кажется немного вонючим.

То же самое касается передачи factory или абстрактного factory через контроллер, а затем необходимость создания новых объектов с помощью чего-то громоздкого типа $this->factory->make('\Data\User'); кажется неудобным. Тем более, что вам нужно закодировать абстрактный factory класс, а затем фактический factory, который связывает зависимости для требуемого объекта.

Ответ 1

Ваш вопрос в приятном вопросе, и мне очень нравятся люди, которые задают вопросы, которые являются здравым смыслом, по причинам "модульного тестирования и ремонтопригодности" (независимо от того, какой из них вы "плох-программист-если-вы-дон" ) t-do-it-themes, это всегда касается модульного тестирования и ремонтопригодности). Поэтому вы задаете правильный вопрос: действительно ли DI поддерживает модульное тестирование и ремонтопригодность, и если да, то как? И предвидеть это: он делает, если используется правильно...

О разложении

Инъекция зависимостей (DI) и инверсия управления (IoC) являются механизмом, который улучшает основные концепции инкапсуляции и разделения проблем ООП. Итак, чтобы ответить на вопрос, нужно утверждать, почему инкапсуляция и разделение проблем - это здорово. Оба являются основными механизмами декомпозиции: инкапсуляция (да, у нас есть модули) и разделение проблем (и у нас есть модули так, как это имеет смысл). Многое можно было бы написать об этой теме, но на данный момент должно быть достаточно сказать, что речь идет о сокращении сложности. Разложение системы позволяет разбить систему - независимо от того, насколько большой - на куски, которыми способен управлять человеческий мозг. Хотя это было немного философски, это действительно важно: если бы не было ограничений человеческого мозга, вся тема поддержки не была бы такой важной. Итак, позвольте сказать: Декомпозиция - это трюк, чтобы уменьшить воспринимаемую сложность системы на куски, которые мы можем справиться.

Но, как всегда, это происходит за счет: Декомпозиция также добавляет сложности, как вы сказали в отношении DI. Так ли это все еще имеет смысл? Да, потому что:

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

Это в основном это, на абстрактном уровне. И это имеет значение: вам нужно выбрать степень декомпозиции и усилия, которые вы тратите на ее достижение, в соответствии с присущей сложностью системы, которую вы строите (или сложностью, которую она может достичь в какой-то день).

Разложение с DI

Что касается DI особенно: согласно вышеизложенному, существуют достаточно небольшие системы, где добавленная сложность DI не оправдывает уменьшенную воспринимаемую сложность. И, к сожалению, каждое отдельное учебное пособие в Интернете касается одного из них, который не поддерживает понимание того, что такое весь fuzz.

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

DI поддерживает разделение What (interface) и How (реализация): если речь идет только о стеклянных дверях, я согласен: если это слишком много для одного мозга, он, вероятно, не должен быть программистом. Но в реальной жизни все сложнее: DI позволяет сосредоточиться на том, что действительно важно: как дом я не забочусь о своей двери, пока я могу полагаться на то, что ее можно закрыть и открыть. Может быть, сейчас нет дверей? Вам просто не нужно заботиться об этом. При регистрации компонентов в контейнере вы можете снова сосредоточиться: какую дверь я хочу в своем доме? Вам больше не нужно заботиться о двери или о доме: они в порядке, вы уже знаете. Вы разделяли озабоченность: определение того, как все происходит вместе (компоненты) и фактически объединяет их (контейнер). Это все, насколько я могу судить по моему опыту. Это звучит неуклюже, но в реальной жизни это большое достижение.

Немного менее философский

Чтобы снова получить это на земле, есть еще ряд практических преимуществ:

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

Вероятно, вы знаете следующее: вы несколько дней работали над чем-то (скажем, стеклянная дверь), и вы гордитесь. Шесть месяцев спустя - вы многому научились - вы снова смотрите на него, и это дерьмо. Вы выбросите его. Без DI вам нужно изменить свой дом, потому что он использует класс, который вы только что испортили. С DI ваш дом не меняется. Он может сидеть в своем собственном собрании: вам даже не нужно перекомпилировать сборку дома, это не коснется. Это, в сложном сценарии, является огромным преимуществом.

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

Ответ 2

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

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

$dsn = '....';
$pdo = new PDO($dsn, $params);

$config_adapter = new MySQL_Config_Adapter($pdo);

$config_manager = new Config_Manager($config_adapter);
// $config_manager is ready to be used

Теперь посмотрим, что произойдет, если мы разрешим классу создавать свои собственные зависимости

class Foo
{
    public function __construct($config = null)
    {
         if ($config !== null) {
             global $pdo;

             $config_adapter = new MySQL_Config_Adapter($pdo);

             $config_manager = new Config_Manager($config_adapter);

             $this->config = $config_manager;
        } else {
             // Ok, it was injected
             $this->config = $config;
        }
    }
}

Здесь есть 3 очевидные проблемы:

  • Глобальное состояние

Итак, вы в основном решаете, хотите ли вы иметь глобальное состояние или нет. Если вы предоставили экземпляр $config, то вы говорите, что не хотите глобального состояния. В противном случае вы говорите, что хотите этого.

  • Плотно муфты

Итак, что, если вы решили переключиться с MySQL на MongoDB или даже на обычный file-based PHP-array на сохранение конфигурации CMS's? Затем вам придется переписать много кода, который отвечает за инициализацию зависимостей.

  • Непонятное нарушение принципа единственной ответственности

У класса должна быть только одна причина для изменения. Класс должен служить только для особых целей. Это означает, что класс Foo имеет более чем одну ответственность - он также отвечает за управление зависимостями.

Как это должно быть сделано правильно?

public function __construct(IConfig $config)
{
    $this->config = $config;
}

Поскольку он не тесно связан с конкретным адаптером, и с тех пор было бы так легко выполнить тестирование на единицу или заменить адаптер (скажем, MySQL на что-то еще)

Как насчет переопределения аргументов по умолчанию?

Если вы переопределяете значение по умолчанию objects, то вы делаете что-то неправильно, и это признак того, что ваш класс делает слишком много.

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

Обратно к примерам кода

Посмотрите на свой пример кода.

   public function __construct(IDoor $door = null, IWindow $window = null, IRoof $roof = null)
   {
      $this->door   = ($door)   ? $door   : new Door;
      $this->window = ($window) ? $window : new Window;
      $this->roof   = ($roof)   ? $roof   : new Roof;
   }

Здесь вы говорите, что-то вроде этого: если некоторые из аргументов не предоставлены, то импортируйте экземпляр этого аргумента из глобальной области. Проблема здесь в том, что ваш House знает, откуда вы связаны, в то время как он должен полностью не знать о такой информации.

Теперь поднимите некоторые сценарии в реальном мире:

  • Что делать, если вы хотите изменить цвет своей двери?
  • Что делать, если вы хотите изменить размер своего окна.
  • Что делать, если вы хотите использовать ту же дверь для другого дома, но разные размеры окна?

Если вы собираетесь придерживаться того, как вы написали код, тогда вы получите дублирование массового кода. С учетом "чистого" DI это будет так же просто, как:

$door = new Door();
$door->setColor('black');

$window = new Window();
$window->setSize(500, 500);

$a_house = new House($door, $window, $roof);

// As I said, I want house2 to have the same door, but different window size
$window->setSize(1000, 1000);

$b_house = new House($door, $window, $roof);

СНОВА: Основная точка инъекции зависимостей заключается в том, что объекты могут использовать одни и те же экземпляры

Еще одна вещь,

Локаторы сервисов/контейнеры IoC отвечают за хранение объектов. Они просто хранят/извлекают объекты, например $pdo.

Заводы просто абстрагируют экземпляр класса.

Итак, Они не являются "частью" инъекции зависимостей, они используют ее.

Что это.

Ответ 3

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

Используя ваш пример: Объекту Roof требуется угол тангажа. Угол по умолчанию зависит от местоположения вашего дома (плоская крыша не работает так хорошо с 10 'снегом) с помощью новых/изменений в бизнес-правилах. Итак, теперь ваш House должен вычислить, какой угол перейти к Roof. Вы можете сделать это либо путем передачи местоположения (которое House в настоящее время требуется только для вычисления угла, либо создает "местоположение по умолчанию" для передачи в конструкторе Roof). В любом случае, конструктор теперь должен выполнить некоторую работу по созданию крыши по умолчанию.

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

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

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