Имея проблемы с пониманием внедрения зависимости - PullRequest
18 голосов
/ 31 марта 2011

Я строю небольшой проект, чтобы попытаться научить себя как можно большему числу основных принципов, что для меня означает, что я не использую готовые фреймворки (как однажды сказал Джефф , «Не заново изобретать колесо, , если вы не планируете больше узнать о колесах"[выделено мной]) и следовать принципам Test Driven Development.

В своем квесте я недавно столкнулся с концепцией внедрения зависимостей, которая, по-видимому, необходима для TDD. Моя проблема в том, что я не могу обернуть голову вокруг этого. Мое понимание до сих пор состоит в том, что «вызывающая сторона передает класс / метод любым другим классам, которые ему могут понадобиться, вместо того, чтобы позволить им создавать их самостоятельно».

У меня есть два примера проблем, которые я пытаюсь решить с помощью DI. Я на правильном пути с этими рефакторингами?

Подключение к базе данных

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

class Post {  
  private $id;  
  private $body;  

  public static function getPostById($id) {  
    $db = Database::getDB();  
    $db->query("SELECT...");  
    //etc.  
    return new Post($id, $body);
  }  

  public function edit($newBody) {  
    $db = Database::getDB();  
    $db->query("UPDATE...");  
    //etc.  
  }  
}  

С DI, я думаю, это будет выглядеть примерно так:

class Post {  
  private $db; // new member

  private $id;  
  private $body;  

  public static function getPostById($id, $db) { // new parameter   
    $db->query("SELECT...");  // uses parameter
    //etc.  
    return new Post($db, $id, $body);
  }  

  public function edit($id, $newBody) {   
    $this->db->query("UPDATE...");  // uses member
    //etc.  
  }  
} 

Я все еще могу использовать синглтон с учетными данными, указанными в настройках приложения, но мне просто нужно передать его из контроллера (контроллеры в любом случае не тестируются модулем):

Post::getPostById(123, Database::getDB);

Модели вызова моделей

Возьмем, к примеру, сообщение с количеством просмотров. Поскольку логика определения того, является ли представление новым, не специфична для объекта Post, он просто должен был быть статическим методом для своего собственного объекта. Объект Post будет вызывать его:

class Post {
  //...

  public function addView() {
    if (PageView::registerView("post", $this->id) {
     $db = Database::getDB();
     $db->query("UPDATE..");
     $this->viewCount++;
   }
}

С DI, я думаю, это выглядит примерно так:

class Post {
  private $db;
  //...

  public function addView($viewRegistry) {
    if ($viewRegistry->registerView("post", $this->id, $this->db) {
     $this->db->query("UPDATE..");
     $this->viewCount++;
   }
}

Это изменяет вызов от контроллера к этому:

$post->addView(new PageView());

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

В этом случае мы углубляемся только на один уровень, поэтому создание экземпляра контроллера всем кажется работоспособным (хотя класс PageView косвенно получает соединение с БД через переменную-член Post), но кажется, что он может получить неудобно, если вам нужно было вызвать метод, который нуждался в классе, который нуждался в классе, который нуждался в классе. Полагаю, это может означать, что это тоже запах кода.

Я на правильном пути с этим, или я полностью неправильно понял DI? Любая критика и предложения приветствуются.

Ответы [ 4 ]

7 голосов
/ 31 марта 2011

Да.Похоже, у вас есть правильная идея.Вы увидите, что когда вы реализуете DI, все ваши зависимости будут всплывать наверх.Наличие всего наверху облегчает насмешку над необходимыми объектами для тестирования.

Наличие класса, которому нужен класс, которому нужен класс, не является плохой вещью.Что вы описываете там, это ваш объект графа.Это нормально для DI.Давайте возьмем объект Дома в качестве примера.Это зависит от кухни;Кухня зависит от раковины;Раковина зависит от Faucet и так далее.Инстанциация Дома выглядела бы как new House(new Kitchen(new Sink(new Faucet()))).Это помогает обеспечить соблюдение принципа единой ответственности.(Кроме того, вы должны выполнить эту работу по созданию экземпляра на чем-то вроде фабрики или застройщика, чтобы дополнительно обеспечить соблюдение принципа единой ответственности.)

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

4 голосов
/ 31 марта 2011

Зависимость инъекций составляет около инъекций . Вам нужно какое-то решение, чтобы внедрить внешний объект.

Традиционные подходы:

  • конструктор инъекций __construnctor($dependecy) {$this->_object = $dependency}
  • сеттер впрыска setObject($dependency) {$this->_object = $dependency}
  • Геттерная инъекция getObject() {return $this->_dependency} и ускорение этого метода, например. из заглушки или издеваться в тестах.

Вы также можете смешать все вышеперечисленное, в зависимости от того, что вам нужно.

Избегайте статических звонков. Мое личное правило - использовать статические только при вызове некоторых функций, например, My::strpos() или при работе с синглетонами или реестром (который должен быть ограничен минимумом, потому что глобальное состояние является злом).

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

Взгляните на другие внедрение зависимостей + [php] тем на SO .

Редактировать после комментария:

Контейнер

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

Контейнер может создавать экземпляры всех ресурсов, которые вам нужны во время загрузки, или лениво загружать ресурс при доступе (лучшее решение).

В качестве примера рассмотрим:

Еще одна замечательная ссылка:

4 голосов
/ 31 марта 2011

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

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

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

Подробнее об этом см.


РЕДАКТИРОВАТЬ: с парой других ответов, предлагающих использовать контейнер DI, я чувствую, что необходимо подчеркнуть, что вам не нужен контейнер DI для выполнения DI. Второй пост в последней ссылке, приведенной выше, обсуждает это.

1 голос
/ 02 апреля 2011

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

http://www.potstuck.com/2009/01/08/php-dependency-injection

Вы поймете, что такое контейнер: $ book = Container :: makeBook ();

Что касается второго примера: в вашем методе addView я бы попытался избежать передачи объекта $ viewRegistry, я бы проверил условие снаружи в контроллере.

...