Почему я должен использовать внедрение зависимости?

95

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

Почему я должен делать следующее?

class Profile {
    public function deactivateProfile(Setting $setting)
    {
        $setting->isActive = false;
    }
}

Вместо следующего?

class Profile {
    public function deactivateProfile()
    {
        $setting = new Setting();
        $setting->isActive = false;
    }
}
Даниил
источник
8
Вы вводите жестко-зависимую зависимость для deactivateProfile () (что плохо). В первом у вас больше разобщенного кода, что облегчает его изменение и тестирование.
Aulis Ronkainen
3
Зачем тебе делать первый? Вы передаете настройку, а затем игнорируете ее значение.
Фил Н Дебланк
57
Я не согласен с отрицательными голосами. В то время как предмет может считаться тривиальным для экспертов, вопрос имеет смысл: если следует использовать инверсию зависимости, то должно быть обоснование для ее использования.
Флатер
13
@PhilNDeBlanc: этот код явно упрощен и на самом деле не указывает на логику реального мира. Тем не менее, deactivateProfileмне кажется, что установка в isActivefalse, не заботясь о своем предыдущем состоянии, является правильным подходом. Вызов метода по своей сути означает, что вы хотите установить его как неактивный, а не получить его текущий (неактивный) активный статус.
Флатер
2
Ваш код не является примером внедрения зависимости или инверсии. Это пример параметризации (который часто намного лучше, чем DI).
jpmc26

Ответы:

104

Преимущество в том, что без внедрения зависимостей ваш класс Profile

  • нужно знать, как создать объект настроек (нарушает принцип единой ответственности)
  • Всегда создает свой объект настроек одинаково (создает тесную связь между ними)

Но с внедрением зависимости

  • Логика создания объектов настроек где-то еще
  • Легко использовать различные виды объектов настроек

Это может показаться (или даже быть) неуместным в данном конкретном случае, но представьте, если мы говорим не об объекте «Настройки», а об объекте «DataStore», который может иметь разные реализации, один для хранения данных в файлах, а другой для хранения их в база данных. А для автоматизированных тестов вам нужна и фиктивная реализация. Теперь вы действительно не хотите, чтобы класс Profile жестко кодировал тот, который он использует, и, что еще более важно, вы действительно не хотите, чтобы класс Profile знал о путях файловой системы, соединениях с БД и паролях, поэтому создание объектов DataStore имеет произойти где - то в другом месте.

Майкл Боргвардт
источник
23
This may seem (or even be) irrelevant in this particular caseЯ думаю, что это очень важно, на самом деле. Как бы вы получили настройки? Многие системы, которые я видел, будут иметь жестко заданный набор настроек по умолчанию и общедоступную конфигурацию, поэтому вам нужно будет загрузить обе и перезаписать некоторые значения общедоступными настройками. Вам может даже понадобиться несколько источников значений по умолчанию. Возможно, вы даже получаете некоторые с диска, другие с БД. Таким образом, вся логика даже для получения настроек может и часто является нетривиальной - определенно не то, что потребляет код, должно заботиться или не беспокоиться.
ВЛАЗ
Мы могли бы также упомянуть, что инициализация объекта для нетривиального компонента, такого как веб-сервис, сделает $setting = new Setting();ужасно неэффективным. Инъекция и создание объекта происходит один раз.
Викингстеве
9
Я думаю, что использование макетов для тестирования должно иметь больший акцент. Скорее всего, если вы посмотрите только на код, он всегда будет объектом «Настройки» и никогда не изменится, поэтому его передача кажется потраченным впустую усилием. Тем не менее, в самый первый раз, когда вы пытаетесь протестировать объект Profile самостоятельно, не требуя также объекта Settings (используя вместо этого фиктивный объект в качестве решения), необходимость становится очень очевидной.
JPhi1618
2
@ JPhi1618 Я думаю, что проблема с подчеркиванием «DI предназначен для модульного тестирования» заключается в том, что это просто приводит к вопросу «зачем мне нужны модульные тесты». Ответ может показаться очевидным для вас, и преимущества, безусловно, есть, но для кого-то, кто только начинает, говоря: «Вы должны сделать эту сложную вещь, чтобы сделать эту другую сложную музыку», как правило, немного выключение Поэтому хорошо упомянуть различные преимущества, которые могут быть более применимы к тому, что они делают сейчас.
IMSoP
1
@ user986730 - это очень хороший момент, и он подчеркивает, что реальный принцип здесь - это инверсия зависимостей , в которой внедрение является лишь одним из методов. Так, например, в C вы не можете реально внедрять зависимости, используя библиотеку IOC, но вы можете инвертировать их во время компиляции, включая различные исходные файлы для ваших макетов и т. д., что имеет тот же эффект.
Стивен Бирн
64

Внедрение зависимостей облегчает тестирование вашего кода.

Я узнал об этом на собственном опыте, когда мне было поручено исправить трудно обнаруживаемую ошибку в интеграции Magento с PayPal.

Когда PayPal сообщает Magento о сбое платежа, возникает проблема: Magento не регистрирует сбой должным образом.

Тестирование потенциального исправления «вручную» было бы очень утомительным: вам нужно было бы каким-то образом вызвать «Неудачное» уведомление PayPal. Вы должны будете отправить электронную проверку, отменить ее и подождать, пока она не выдаст ошибку. Это означает, что для проверки односимвольного изменения кода требуется более 3 дней!

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

<?php
// This is the dependency we will inject to facilitate our testing
class MockHttpClient extends Varien_Http_Adapter_Curl {
    function read() {
        // Make Magento think that PayPal said "VERIFIED", no matter what they actually said...
        return "HTTP/1.1 200 OK\n\nVERIFIED";
    }
}

// Here, we trick Magento into thinking PayPal actually sent something back.
// Magento will try to verify it against PayPal's API though, and since it's fake data, it'll always fail.
$ipnPayload = array (
  'invoice'        => '100058137',         // Order ID to test against
  'txn_id'         => '04S87540L2309371A', // Test PayPal transaction ID
  'payment_status' => 'Failed'             // New payment status that Magento should ingest
);

// This is what Magento's controller calls during a normal IPN request.
// Instead of letting Magento talk to PayPal, we "inject" our fake HTTP client, which always returns VERIFIED.
Mage::getModel('paypal/ipn')->processIpnRequest($ipnPayload, new MockHttpClient());

Я уверен, что у модели DI есть много других преимуществ, но увеличенная тестируемость - единственное большое преимущество в моем уме .

Если вам интересно решение этой проблемы, посмотрите репозиторий GitHub здесь: https://github.com/bubbleupdev/BUCorefix_Paypalstatus

Эрик Систранд
источник
4
Внедрение зависимостей облегчает тестирование кода, чем код с жестко закодированными зависимостями. Устранить зависимости от бизнес-логики в целом еще лучше.
Ant P
1
И один из основных способов сделать то, что предлагает @AntP - это параметризация . Логика преобразования результатов, возвращаемых из базы данных, в объект, используемый для заполнения шаблона страницы (обычно называемый «моделью»), не должна даже знать, что происходит выборка; ему просто нужно получить эти объекты в качестве входных данных.
jpmc26
4
@ jpmc26 действительно - я склонен говорить о функциональном ядре, императивной оболочке , которая на самом деле является просто причудливым названием для передачи данных в ваш домен, а не для введения в него зависимостей. Домен является чистым, его можно тестировать модульно, без всяких насмешек, а затем просто обернуть в оболочку, которая адаптирует такие вещи, как постоянство, обмен сообщениями и т. Д.
Ant P
1
Я думаю, что единственный акцент на тестируемости вреден для принятия DI. Это делает его непривлекательным для людей, которые чувствуют, что им не нужно много тестировать или они думают, что тестирование уже под контролем. Я бы сказал, что практически невозможно написать чистый код многократного использования без DI. Тестирование очень далеко в списке преимуществ, и разочаровывает тот факт, что этот ответ занимает 1 или 2 место в каждом вопросе о преимуществах DI.
Карл Лет
26

Почему (что даже проблема)?

Почему я должен использовать внедрение зависимости?

Лучшая мнемоника, которую я нашел для этого, - « новое - это клей »: каждый раз, когда вы используете newв своем коде, этот код привязан к конкретной реализации . Если вы неоднократно используете new в конструкторах, вы создадите цепочку конкретных реализаций . И поскольку вы не можете «иметь» экземпляр класса, не создав его, вы не можете отделить эту цепочку.

В качестве примера представьте, что вы пишете видеоигру для гоночных автомобилей. Вы начали с класса Game, который создает RaceTrack, который создает 8 Cars, каждый из которых создает Motor. Теперь , если вы хотите 4 дополнительных Carsс другим ускорением, вам придется изменить каждый класс упоминается , за исключением , может быть Game.

Более чистый код

Это только для более чистой архитектуры / кода

Да .

Тем не менее, это может показаться менее ясным в этой ситуации, потому что это скорее пример того, как это сделать . Фактическое преимущество проявляется только в том случае, если задействованы несколько классов, и его труднее продемонстрировать, но представьте, что вы использовали DI в предыдущем примере. Код, создающий все эти вещи, может выглядеть примерно так:

List<Car> cars = new List<Car>();
for(int i=0; i<8; i++){
    float acceleration = 0.3f;
    float maxSpeed = 200.0f;
    Motor motor = new Motor(acceleration, maxSpeed);
    Car car = new Car(motor);
    cars.Add(car);
}
RaceTrack raceTrack = new RaceTrack(cars);
Game game = new Game(raceTrack);

Добавление этих 4 различных автомобилей теперь можно сделать, добавив следующие строки:

for(int i=0; i<4; i++){
    float acceleration = 0.5f;
    float maxSpeed = 100.0f;
    Motor motor = new Motor(acceleration, maxSpeed);
    Car car = new Car(motor);
    cars.Add(car);
}
  • Никаких изменений RaceTrack, Game, Car, или Motorбыли необходимы - это означает , что мы можем быть 100% уверены , что мы не вводить никаких новых ошибок там!
  • Вместо того, чтобы переходить между несколькими файлами, вы сможете одновременно видеть все изменения на экране. Это результат того, что создание / настройка / конфигурация рассматривается как собственная ответственность , а не сборка двигателя для машины.

Вопросы производительности

или это влияет на производительность в целом?

Нет . Но, если быть полностью честным с тобой, это возможно.

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

В 99,999% * случаев производительность будет выше, потому что вы потратили меньше времени на исправление ошибок и больше времени на улучшение ресурсоемких алгоритмов.

* полностью составленный номер

Добавлена ​​информация: "жестко закодировано"

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

Чтобы сделать это, вы должны добавить код для чтения файла и последующего анализа JSON. Если вы рассмотрите пример еще раз; в не-DI версии a Carили a Motorтеперь должны прочитать файл. Это не звучит так, как будто в этом слишком много смысла.

В версии DI вы бы добавили его в код, настраивающий игру.

Р. Шмитц
источник
3
Объявление жестко закодировано, между кодом и файлом конфигурации не так уж много различий. Файл, связанный с приложением, является источником, даже если вы читаете динамически. Извлечение значений из кода в файлы данных в формате json или в любом другом формате «config» ничего не поможет, если только значения не должны быть переопределены пользователем или не зависят от среды.
Ян Худек
3
Я фактически сделал тамагочи однажды на Arduino (16 МГц, 2 КБ)
Jungkook
@JanHudec Правда. У меня там было более длинное объяснение, но я решил убрать его, чтобы оно было короче и сосредоточилось на том, как оно связано с DI. Есть еще вещи, которые не на 100% правильные; В целом, ответ более оптимизирован, чтобы подтолкнуть «точку» DI, не делая его слишком длинным. Или, иначе говоря, это то, что я хотел бы услышать, когда начинал с DI.
Р. Шмитц
17

Я всегда был озадачен введением зависимости. Казалось, что он существует только в сферах Java, но эти сферы говорили об этом с большим почтением. Видите ли, это был один из великих образцов, которые, как говорят, наводят порядок в хаосе. Но примеры всегда были замысловатыми и искусственными, создавая проблему, а затем решая ее путем усложнения кода.

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

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

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

Итак: Есть ли веская причина, по которой вам конкретно нужно использовать именно этот Settingтип / значение? Есть ли веская причина, по которой вызывающий код может захотеть другой Settingтип / значение? (Помните, тесты - это код !)

Eevee
источник
2
Ага. IoC наконец-то щелкнуло мной, когда я понял, что это просто «позволить вызывающему абоненту принимать решения». То, что IoC означает, что управление компонентом переходит от автора компонента к пользователю компонента. И поскольку на тот момент у меня уже было достаточно проблем с программным обеспечением, которое считалось более умным, чем я, я сразу же был продан на подходе DI.
Joker_vD
1
«это просто передача аргументов функциям. Это всего лишь шаблон». Это единственное хорошее или даже понятное объяснение ДИ, которое я слышал (ну, большинство людей даже не говорят о том, что это такое , а скорее о его преимуществах). И затем они используют сложный жаргон, такой как «инверсия управления» и «слабая связь», которые просто требуют больше вопросов, чтобы понять, когда это действительно простая идея.
Эддисон
7

Пример, который вы приводите, не является инъекцией зависимостей в классическом смысле. Внедрение зависимостей обычно относится к передаче объектов в конструкторе или с помощью «установки сеттера» сразу после создания объекта, чтобы установить значение для поля во вновь созданном объекте.

Ваш пример передает объект в качестве аргумента методу экземпляра. Этот метод экземпляра затем изменяет поле в этом объекте. Внедрение зависимости? Разрушать инкапсуляцию и скрывать данные? Абсолютно!

Теперь, если код был такой:

class Profile {
    private $settings;

    public function __construct(Settings $settings) {
        $this->settings = $settings;
    }

    public function deactive() {
        $this->settings->isActive = false;
    }
}

Тогда я бы сказал, что вы используете внедрение зависимостей. Заметным отличием является то, что Settingsобъект передается в конструктор или Profileобъект.

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

Поскольку вы непосредственно обращаетесь к полю объекта «Настройки», а не вызываете метод, вы не можете воспользоваться преимуществом Полиморфизма, который является одним из преимуществ внедрения зависимостей.

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

  1. Создание экземпляра объекта настроек внутри конструктора профиля

  2. Передайте объект Settings в конструкторе и скопируйте отдельные поля, которые относятся к профилю.

Честно говоря, если передать объект «Настройки» в deactivateProfileи затем изменить внутреннее поле объекта «Настройки», это запах кода. Объект Settings должен быть единственным, изменяющим его внутренние поля.

Грег Бургхардт
источник
5
The example you give is not dependency injection in the classical sense.- Это не имеет значения. У ОО людей есть объекты в мозгу, но вы все равно передаете зависимость чему-то.
Роберт Харви
1
Когда вы говорите о « в классическом смысле », вы, как говорит @RobertHarvey, говорите чисто в терминах ОО. Например, в функциональном программировании внедрение одной функции в другую (функции более высокого порядка) является классическим примером внедрения зависимости в эту парадигму.
Дэвид Арно
3
@RobertHarvey: Я полагаю, что слишком много вольности использовал с «инъекцией зависимости». Место, в котором люди чаще всего думают об этом термине и используют его, относится к полям объекта, который «вводится» во время создания объекта или сразу после него в случае установки сеттера.
Грег Бургхардт
@DavidArno: Да, вы правы. Похоже, у OP есть объектно-ориентированный фрагмент кода PHP в этом вопросе, поэтому я отвечал только в этом отношении, а не касался функционального программирования - хотя с помощью PHP вы могли бы задать тот же вопрос с точки зрения функционального программирования.
Грег Бургхардт
7

Я знаю, что опаздываю на эту вечеринку, но чувствую, что упущен важный момент.

Зачем мне это делать:

class Profile {
    public function deactivateProfile(Setting $setting)
    {
        $setting->isActive = false;
    }
}

Ты не должен. Но не потому, что Dependency Injection - плохая идея. Это потому, что это делает это неправильно.

Давайте посмотрим на это с помощью кода. Мы собираемся сделать это:

$profile = new Profile();
$profile->deactivateProfile($setting);

когда мы получаем примерно то же самое из этого:

$setting->isActive = false; // Deactivate profile

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

Теперь, что если бы вместо этого у нас было это:

$profile = new Profile($setting);

$application = new Application($profile);

$application.start();

И теперь applicationон может активировать и деактивировать profileбез необходимости знать что-либо конкретное о том, settingчто он на самом деле меняется. Почему это хорошо? В случае, если вам нужно изменить настройки. Они applicationзащищены от этих изменений, поэтому вы можете сойти с ума в безопасном замкнутом пространстве, не наблюдая, как все ломается, как только вы что-то трогаете.

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

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

Попробуйте это на чем-то, что вы должны поддерживать со временем, и посмотрите, не поможет ли это.

candied_orange
источник
6

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

Как владелец гаража, когда вы поручаете механику что-то делать с автомобилем, ожидаете ли вы, что механик создаст свою собственную отвертку / гаечный ключ / детали автомобиля? Нет, вы предоставляете механику необходимые ему детали / инструменты

Почему мы это делаем? Мы подумаем. Вы владелец гаража, который хочет нанять кого-то, чтобы стать вашим механиком. Вы научите их быть механиком (= вы напишите код).

Что будет проще:

  • Научите механика, как прикрепить спойлер к машине с помощью отвертки.
  • Научите механика создавать автомобиль, создать спойлер, создать отвертку, а затем прикрепить вновь созданный спойлер к вновь созданному автомобилю с помощью вновь созданной отвертки.

Есть огромные преимущества, если ваш механик не создает все с нуля:

  • Очевидно, что обучение (= разработка) резко сокращается, если вы просто снабжаете своего механика существующими инструментами и деталями.
  • Если один и тот же механик должен выполнять одну и ту же работу несколько раз, вы можете убедиться, что он повторно использует отвертку вместо того, чтобы всегда выбрасывать старую и создавать новую.
  • Кроме того, механик, который научился создавать все, должен быть гораздо большим экспертом и, следовательно, ожидать более высокую заработную плату. Здесь аналогия кодирования заключается в том, что поддерживать класс со многими обязанностями гораздо сложнее, чем класс с одной строго определенной ответственностью.
  • Кроме того, когда новые изобретения появляются на рынке, спойлеры теперь делаются из углерода, а не из пластика; Вы должны будете переобучить (= перестроить) своего опытного механика. Но ваш «простой» механик не нужно будет переучивать, если спойлер все еще можно прикрепить таким же образом.
  • Наличие механика, который не полагается на автомобиль, который они сами построили, означает, что у вас есть механик, способный справиться с любым автомобилем, который они могут получить. Включая автомобили, которые еще не существовали на момент обучения механика. Тем не менее, ваш опытный механик не сможет строить новые автомобили, которые были созданы после их обучения.

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

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

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


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

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

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


это влияет на производительность в целом?

Это не влияет на производительность приложения во время выполнения. Но это сильно влияет на время разработки (и, следовательно, производительность) разработчика .

Flater
источник
2
Если вы удалили свой комментарий: « Как незначительный комментарий, ваш вопрос фокусируется на инверсии зависимостей, а не на внедрении зависимостей. Инъекция - это один из способов инверсии, но это не единственный способ». Это был бы отличный ответ. Инверсия зависимости может быть достигнута либо с помощью инъекции, либо с помощью локатора / глобалов Примеры вопроса касаются инъекций. Так что вопрос является о зависимости инъекций (а также инверсии зависимостей).
Дэвид Арно,
12
Я думаю, что вся эта машина немного изуродована и сбита с толку
Ewan
4
@ Флатер, часть проблемы заключается в том, что никто, похоже, не согласен с тем, в чем разница между внедрением зависимостей, инверсией зависимостей и инверсией управления. Однако одно можно сказать наверняка: «контейнер», безусловно, не нужен для введения зависимости в метод или конструктор. Чистый (или бедный) DI конкретно описывает ручную инъекцию зависимостей. Это единственная инъекция зависимостей, которую я лично использую, поскольку мне не нравится «магия», связанная с контейнерами.
Дэвид Арно
1
@Flater Проблема с примером состоит в том, что вы почти наверняка не смоделировали бы какую-либо проблему в коде таким образом. Таким образом, это неестественно, вынужденно и добавляет больше вопросов, чем ответов. Это делает его более вероятным ввести в заблуждение и запутать, чем продемонстрировать.
jpmc26
2
@gbjbaanb: Ни в коем случае мой ответ даже отдаленно не углубляется в полиморфизм. Не существует наследования, реализации интерфейса или чего-либо похожего на увеличение / уменьшение типов. Вы совершенно неправильно понимаете ответ.
Flater
0

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

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

Низкая связь

Связь в компьютерном программировании :

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

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

Один класс, создающий объекты для других, является очень сильной связью, потому что один должен знать о существовании другого; он должен знать, как создать его экземпляр (какие аргументы нужны конструктору), и эти аргументы должны быть доступны при вызове конструктора. Кроме того, в зависимости от того, нуждается ли язык в явной деконструкции (C ++), это приведет к дополнительным сложностям. Если вы вводите новые классы (то есть, a NextSettingsили что-то еще), вы должны вернуться к исходному классу и добавить дополнительные вызовы к их конструкторам.

Высокая сплоченность

Сплоченность :

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

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

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

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

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

Anoe
источник
-1

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

Инверсия или внедрение зависимостей - это метод, который позволяет вашему коду решать, какая реализация метода вызывается во время выполнения. Это максимизирует преимущества позднего связывания. Техника необходима, когда язык не поддерживает замену неэкземплярных функций во время выполнения. Например, в Java отсутствует механизм для замены вызовов статического метода вызовами другой реализации; в отличие от Python, где все, что необходимо для замены вызова функции, - это привязать имя к другой функции (переназначить переменную, содержащую функцию).

Почему мы хотим варьировать реализацию функции? Есть две основные причины:

  • Мы хотим использовать подделки для тестирования. Это позволяет нам тестировать класс, который зависит от выборки из базы данных, без фактического подключения к базе данных.
  • Нам нужно поддерживать несколько реализаций. Например, нам может потребоваться настроить систему, которая поддерживает базы данных MySQL и PostgreSQL.

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

thing5 =  new MyThing5();
thing3 = new MyThing3(thing5, new MyThing10());

myApp = new MyApp(
    new MyAppDependency1(thing5, thing3),
    new MyAppDependency2(
        new Thing1(),
        new Thing2(new Thing3(thing5, new Thing4(thing5)))
    ),
    ...
    new MyAppDependency15(thing5)
);

Это позволяет вам зарегистрировать ваши классы, а затем делает конструкцию для вас:

injector.register(Thing1); // Yes, you'd need some kind of actual class reference.
injector.register(Thing2);
...
injector.register(MyAppDepdency15);
injector.register(MyApp);

myApp = injector.create(MyApp); // The injector fills in all the construction parameters.

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

Слово предостережения

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

myAverageAboveMin()
{
    dbConn = new DbConnection("my connection string");
    dbQuery = dbConn.makeQuery();
    dbQuery.Command = "SELECT * FROM MY_DATA WHERE x > :min";
    dbQuery.setParam("min", 5);
    dbQuery.Execute();
    myData = dbQuery.getAll();
    count = 0;
    total = 0;
    foreach (row in myData)
    {
        count++;
        total += row.x;
    }

    return total / count;
}

Мы могли бы использовать инверсию зависимостей для некоторых частей этого метода:

class MyQuerier
{
    private _dbConn;

    MyQueries(dbConn) { this._dbConn = dbConn; }

    fetchAboveMin(min)
    {
        dbQuery = this._dbConn.makeQuery();
        dbQuery.Command = "SELECT * FROM MY_DATA WHERE x > :min";
        dbQuery.setParam("min", min);
        dbQuery.Execute();
        return dbQuery.getAll();
    }
}


class Averager
{
    private _querier;

    Averager(querier) { this._querier = querier; }

    myAverageAboveMin(min)
    {
        myData = this._querier.fetchAboveMin(min);
        count = 0;
        total = 0;
        foreach (row in myData)
        {
            count++;
            total += row.x;
        }

        return total / count;
    }

Но мы не должны, по крайней мере, не полностью. Обратите внимание, что мы создали класс с состояниемQuerier . Теперь он содержит ссылку на некоторый по существу глобальный объект соединения. Это создает проблемы, такие как трудности в понимании общего состояния программы и того, как разные классы координируются друг с другом. Также обратите внимание, что мы вынуждены выдумывать запрос или соединение, если хотим проверить логику усреднения. Кроме того, лучшим подходом было бы увеличение параметризации :

class MyQuerier
{
    fetchAboveMin(dbConn, min)
    {
        dbQuery = dbConn.makeQuery();
        dbQuery.Command = "SELECT * FROM MY_DATA WHERE x > :min";
        dbQuery.setParam("min", min);
        dbQuery.Execute();
        return dbQuery.getAll();
    }
}


class Averager
{
    averageData(myData)
    {
        count = 0;
        total = 0;
        foreach (row in myData)
        {
            count++;
            total += row.x;
        }

        return total / count;
    }

class StuffDoer
{
    private _querier;
    private _averager;

    StuffDoer(querier, averager)
    {
        this._querier = querier;
        this._averager = averager;
    }

    myAverageAboveMin(dbConn, min)
    {
        myData = this._querier.fetchAboveMin(dbConn, min);
        return this._averager.averageData(myData);
    }
}

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

Теперь мы можем проверить логику усреднения полностью независимо от запросов, и, более того, мы можем использовать ее в более широком диапазоне ситуаций. Мы могли бы подвергнуть сомнению , даже ли нам нужны MyQuerierи Averagerобъекты, а может быть , ответ в том , что мы не делаем , если мы не намерены модульное тестирование StuffDoer, а не модульное тестирование StuffDoerбыло бы вполне разумно , так как он так тесно связан с базой данных. Возможно, имеет смысл просто позволить интеграционным тестам охватить это. В этом случае, мы могли бы хорошо работать fetchAboveMinи averageDataв статических методах.

jpmc26
источник
2
« Внедрение зависимостей - это метод, который поможет вам избежать огромных, запутанных строительных деревьев ... ». Ваш первый пример после этого утверждения - пример инъекции зависимостей в чистом виде или бедняке. Второй пример использования контейнера IoC для «автоматизации» внедрения этих зависимостей. Оба являются примерами внедрения зависимости в действие.
Дэвид Арно
@DavidArno Да, ты прав. Я скорректировал терминологию.
jpmc26
Существует третья основная причина для изменения реализации или, по крайней мере, для разработки кода, предполагая, что реализация может изменяться: это стимулирует разработчика к слабой связи и избегает написания кода, который будет трудно изменить, если в какой-то момент будет реализована новая реализация будущее. И хотя это может не быть приоритетом в некоторых проектах (например, зная, что приложение никогда не будет пересматриваться после его первоначального выпуска), оно будет в других (например, когда бизнес-модель компании специально пытается предложить расширенную поддержку / расширение своих приложений). ).
Флейтер
Инъекция зависимости @Flater все еще приводит к сильной связи. Он связывает логику с конкретным интерфейсом и требует, чтобы рассматриваемый код знал о том, что делает этот интерфейс. Рассмотрим код, который преобразует результаты, полученные из БД. Если я использую DI для их разделения, то коду все равно нужно знать, что происходит выборка из БД, и вызывать его. Лучшее решение - если код преобразования даже не знает, что происходит выборка . Единственный способ, которым вы можете это сделать, - это если результаты выборки передаются вызывающим, а не вводят сборщик.
jpmc26
1
@ jpmc26 Но в вашем примере (комментарий) база данных даже не должна быть зависимостью для начала. Конечно, избегать зависимостей лучше для слабой связи, но DI фокусируется на реализации необходимых зависимостей.
Флатер