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

109

Я читаю о внедрении зависимости (DI). Для меня это очень сложная вещь, так как я читал, что она также ссылается на инверсию управления (IoC), и я чувствовал, что собираюсь отправиться в путешествие.

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

Для меня это просто передача аргумента. Должно быть, мисс поняла суть? Может быть, это станет более очевидным с большими проектами?

Мое понимание не DI (используя псевдокод):

public void Start()
{
    MyClass class = new MyClass();
}

...

public MyClass()
{
    this.MyInterface = new MyInterface(); 
}

И DI будет

public void Start()
{
    MyInterface myInterface = new MyInterface();
    MyClass class = new MyClass(myInterface);
}

...

public MyClass(MyInterface myInterface)
{
    this.MyInterface = myInterface; 
}

Может ли кто-то пролить свет, я уверен, что я в замешательстве здесь.

MyDaftQuestions
источник
5
Попробуйте иметь 5 из MyInterface и MyClass AND, чтобы иметь несколько классов, образующих цепочку зависимостей. Это становится болью в заднице, чтобы настроить все.
Euphoric
159
« Для меня это просто передача аргумента. Должно быть, я неправильно понял суть? » Нет. Ты понял. Это «внедрение зависимости». Теперь посмотрим, какой еще сумасшедший жаргон вы можете придумать для простых понятий, это весело!
Эрик Липперт
8
Я не могу достаточно высоко оценить комментарий мистера Липперта. Я тоже обнаружил, что это все равно что сбивать с толку жаргон для чего-то, что я делал естественно.
Алекс Хамфри
16
@EricLippert: Хотя это правильно, я думаю, что это немного сокращает. Parameters as-DI не является простой концепцией для программиста со средним императивом / OO, который привык передавать данные. Я здесь позволяю вам обмениваться поведением , что требует более гибкого мировоззрения, чем у посредственного программиста.
Phoshi
14
@Phoshi: Вы делаете хорошее замечание, но вы также указали на то, почему я скептически отношусь к тому, что «внедрение зависимости» - это хорошая идея. Если я напишу класс, который зависит от его корректности и производительности от поведения другого класса, то последнее, что я хочу сделать, - это заставить пользователя класса отвечать за правильное построение и настройку зависимости! Это моя работа. Каждый раз, когда вы позволяете потребителю вводить зависимость, вы создаете точку, в которой потребитель может сделать это неправильно.
Эрик Липперт

Ответы:

106

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

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

Стефан Биллиет
источник
13
Это здорово, хорошо объяснено. Пожалуйста, примите мнимое +1, так как мой представитель пока не допустит этого!
MyDaftQuestions
11
Сложный сценарий строительства является хорошим кандидатом на использование шаблона Factory. Таким образом, фабрика берет на себя ответственность за создание объекта, так что сам класс не должен этого делать, и вы придерживаетесь принципа единой ответственности.
Мэтт Гибсон
1
@MattGibson хороший момент.
Стефан Биллиет,
2
+1 для тестирования, хорошо структурированный DI поможет как ускорить тестирование, так и сделать юнит-тесты, содержащиеся в классе, который вы тестируете.
Umur Kontacı
52

Как DI может быть реализован, очень сильно зависит от используемого языка.

Вот простой пример без DI:

class Foo {
    private Bar bar;
    private Qux qux;

    public Foo() {
        bar = new Bar();
        qux = new Qux();
    }
}

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

class Foo {
    private Bar bar;
    private Qux qux;

    public Foo(Bar bar, Qux qux) {
        this.bar = bar;
        this.qux = qux;
    }
}

// in production:
new Foo(new Bar(), new Qux());
// in test:
new Foo(new BarMock(), new Qux());

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

Мы можем ввести больше абстракций, используя фабрики:

  • Один из вариантов - Fooгенерировать абстрактную фабрику

    interface FooFactory {
        public Foo makeFoo();
    }
    
    class ProductionFooFactory implements FooFactory {
        public Foo makeFoo() { return new Foo(new Bar(), new Baz()) }
    }
    
    class TestFooFactory implements FooFactory {
        public Foo makeFoo() { return new Foo(new BarMock(), new Baz()) }
    }
    
    FooFactory fac = ...; // depends on test or production
    Foo foo = fac.makeFoo();
  • Другой вариант - передать фабрику конструктору:

    interface DependencyManager {
        public Bar makeBar();
        public Qux makeQux();
    }
    class ProductionDM implements DependencyManager {
        public Bar makeBar() { return new Bar() }
        public Qux makeQux() { return new Qux() }
    }
    class TestDM implements DependencyManager {
        public Bar makeBar() { return new BarMock() }
        public Qux makeQux() { return new Qux() }
    }
    
    class Foo {
        private Bar bar;
        private Qux qux;
    
        public Foo(DependencyManager dm) {
            bar = dm.makeBar();
            qux = dm.makeQux();
        }
    }

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

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

package Foo {
    use signatures;

    sub new($class, $dm) {
        return bless {
            bar => $dm->{bar}->new,
            qux => $dm->{qux}->new,
        } => $class;
    }
}

my $prod = { bar => 'My::Bar', qux => 'My::Qux' };
my $test = { bar => 'BarMock', qux => 'QuxMock' };
$test->{bar} = 'OtherBarMock';  # change conf at runtime

my $foo = Foo->new(rand > 0.5 ? $prod : $test);

В таких языках, как Java, менеджер зависимостей может вести себя подобно Map<Class, Object>:

Bar bar = dm.make(Bar.class);

Для какого фактического класса Bar.classразрешается, может быть сконфигурирован во время выполнения, например, поддерживая, Map<Class, Class>который сопоставляет интерфейсы с реализациями.

Map<Class, Class> dependencies = ...;

public <T> T make(Class<T> c) throws ... {
    // plus a lot more error checking...
    return dependencies.get(c).newInstance();
}

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

class Foo {
    @Inject(Bar.class)
    private Bar bar;

    @Inject(Qux.class)
    private Qux qux;

    ...
}

dm.make(Foo.class);  // takes care of initializing "bar" and "qux"

Вот пример реализации крошечной (и очень ограниченной) структуры DI: http://ideone.com/b2ubuF , хотя эта реализация полностью непригодна для неизменяемых объектов (эта наивная реализация не может принимать никаких параметров для конструктора).

Амон
источник
7
Это супер с отличными примерами, хотя мне понадобится несколько раз, чтобы прочитать его, чтобы все это переварить, но спасибо, что нашли так много времени.
MyDaftQuestions
33
Забавно,
48

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

«Внедрение зависимостей» - это понятие за 5 центов стоимостью 25 долларов. (...) Внедрение зависимостей означает предоставление объекту его переменных экземпляра. (...).

из блога Джеймса Шора . Конечно, есть более сложные версии / шаблоны, наложенные поверх этого, но этого достаточно, чтобы в основном понять, что происходит. Вместо объекта, создающего свои собственные переменные экземпляра, они передаются извне.

user967543
источник
10
Я читал это ... и до сих пор это не имело смысла ... Теперь это имеет смысл. Зависимость на самом деле не настолько большая концепция, чтобы ее понять (независимо от того, насколько она мощная), но у нее есть громкое имя и много интернет-шума, связанного с ней!
MyDaftQuestions
18

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

Идея DI состоит в том, чтобы использовать подход «плагин» везде. Это может показаться не таким уж большим делом, если вы живете в простой лачуге без гипсокартона и всех электрических проводов. (Вы разнорабочий - вы можете изменить что угодно!) И точно так же, в небольшом и простом приложении DI может добавить больше сложностей, чем оно того стоит.

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

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

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

public void Main()
{
    ILightFixture fixture = new ClassyChandelier();
    MyRoom room = new MyRoom (fixture);
}
...
public MyRoom(ILightFixture fixture)
{
    this.MyLightFixture = fixture ; 
}

Но СЕЙЧАС, поскольку MyRoomон предназначен для интерфейса ILightFixture , я могу легко перейти в следующий год и изменить одну строку в функции Main («вставить» другую «зависимость»), и я сразу получу новую функциональность в MyRoom (без необходимости перестройте или повторно разверните MyRoom, если он находится в отдельной библиотеке. Это, конечно, предполагает, что все ваши реализации ILightFixture разработаны с учетом подстановки Лискова). Все, с помощью простого изменения:

public void Main()
{
    ILightFixture fixture = new MuchLessFormalLightFixture();
    MyRoom room = new MyRoom (fixture);
}

Кроме того , я могу теперь Test Unit MyRoom(вы это модульное тестирование, не так ли?) И использовать «ложный» светильник, который позволяет мне проверить MyRoomполностью независимым отClassyChandelier.

Конечно, есть еще много преимуществ, но именно они продали эту идею мне.

kmote
источник
Я хотел сказать спасибо, это было полезно для меня, но они не хотят, чтобы вы это делали, а просто используют комментарии, чтобы предлагать улучшения, поэтому, если вам так хочется, вы можете добавить одно из других упомянутых вами преимуществ. Но в остальном, спасибо, это было полезно для меня.
Стив
2
Мне также интересна книга, на которую вы ссылались, но меня тревожит, что на эту тему есть книга на 584 страницы.
Стив
4

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

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

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

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

public void Start()
{
    MySubSubInterfaceA mySubSubInterfaceA = new mySubSubInterfaceA();
    MySubSubInterfaceB mySubSubInterfaceB = new mySubSubInterfaceB();     
    MySubInterface mySubInterface = new MySubInterface(mySubSubInterfaceA,mySubSubInterfaceB);
    MyInterface myInterface = new MyInterface(MySubInterface);
    MyClass class = new MyClass(myInterface);
}

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

Кроме того, представьте класс, который, скажем, реализует IDisposable. Если классы, использующие его, получают его с помощью инъекции, а не создают его самостоятельно, как они узнают, когда вызывать Dispose ()? (Это еще одна проблема, с которой сталкивается контейнер DI.)

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

PSR
источник
Это много сабвуферов и интерфейсов! Вы хотите обновить интерфейс вместо класса, который реализует интерфейс? Кажется неправильным
JBRWilkinson
@JBRWilkinson Я имею в виду класс, который реализует интерфейс. Я должен сделать это яснее. Однако дело в том, что у большого приложения будут классы с зависимостями, которые, в свою очередь, имеют зависимости, которые, в свою очередь, имеют зависимости ... Настройка всего этого вручную в методе Main является результатом выполнения большого количества инъекций конструктора.
PSR
4

На уровне класса это легко.

«Внедрение зависимостей» просто отвечает на вопрос «как я найду моих сотрудников»: «Они на тебя давят - тебе не нужно идти и брать их самому». (Это похоже - но не то же самое, что и «Инверсия контроля», где на вопрос «как я упорядочу свои операции над своими входами?» Есть аналогичный ответ).

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

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

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

И это хорошо.

На уровне приложения ...

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

... вам нужно «гибко» настраивать объект-граф вашего приложения ...

В зависимости от ваших других (не связанных с кодом) целей, вы можете захотеть дать конечному пользователю некоторый контроль над развернутым графом объектов. Это ведет вас в направлении схемы конфигурации того или иного типа, будь то текстовый файл вашего собственного дизайна с некоторыми парами имя / значение, файл XML, пользовательский DSL, декларативный язык описания графов, такой как YAML, императивный язык сценариев, такой как JavaScript, или что-то еще, соответствующее поставленной задаче. Все, что нужно для составления правильного графа объектов, таким образом, чтобы удовлетворить потребности ваших пользователей.

... может быть значительным конструктивным фактором.

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

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

Вы бедняга!

Поскольку вам не хотелось писать все это самостоятельно (а если серьезно, проверьте привязку YAML для вашего языка), вы используете какую-то инфраструктуру DI.

В зависимости от зрелости этой среды у вас может не быть возможности использовать конструктор-инъекцию, даже когда это имеет смысл (соавторы не меняются в течение срока службы объекта), что вынуждает вас использовать Setter Injection (даже когда коллабораторы не изменяются в течение всего срока службы объекта, и даже если на самом деле нет логической причины, по которой все конкретные реализации интерфейса должны иметь коллабораторов определенного типа). Если это так, то вы в настоящее время находитесь в аду сильной связи, несмотря на то, что старательно «использовали интерфейсы» по всей вашей кодовой базе - ужас!

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

Bootnote

Ни в коем случае вы никогда не используете Google Guice, который дает вам кодеру способ избавиться от задачи составления графов объектов с помощью кода ... путем написания кода. Argh!

Дэвид Баллок
источник
0

Многие люди говорили, что DI - это механизм для инициализации переменных экземпляра.

Для очевидных и простых примеров вам не нужно «внедрение зависимостей». Вы можете сделать это с помощью простой передачи параметров конструктору, как вы отметили в вопросе.

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

Например: соединение с базой данных, контекст безопасности, средство ведения журнала, файл конфигурации и т. Д.

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

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

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

gamliela
источник
-2

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

DavidB
источник
-4

Это еще один вариант в дополнение к ответу амона

Используйте Builders:

класс Foo {
    частный бар-бар;
    приватный Qux qux;

    private Foo () {
    }

    public Foo (Builder builder) {
        this.bar = builder.bar;
        this.qux = builder.qux;
    }

    открытый статический класс Builder {
        частный бар-бар;
        приватный Qux qux;
        public Buider setBar (Бар-бар) {
            this.bar = bar;
            верни это;
        }
        public Builder setQux (Qux qux) {
            this.qux = qux;
            верни это;
        }
        public Foo build () {
            вернуть новый Foo (это);
        }
    }
}

// в производстве:
Foo foo = new Foo.Builder ()
    .setBar (новый бар ())
    .setQux (новый Qux ())
    .build ();

// в тесте:
Foo testFoo = new Foo.Builder ()
    .setBar (новый BarMock ())
    .setQux (новый Qux ())
    .build ();

Это то, что я использую для зависимостей. Я не использую DI-фреймворк. Они мешают.

user3155341
источник
4
Это мало что добавляет к другим ответам, полученным год назад, и не дает объяснения того, почему сборщик может помочь спрашивающему понять внедрение зависимости.
@ Снеговик Это ДРУГОЙ вариант вместо DI. Мне жаль, что ты так не видишь. Спасибо за отрицательное голосование.
user3155341
1
спрашивающий хотел понять, действительно ли DI настолько прост, как передача параметра, он не просил альтернативы DI.
1
Примеры ОП очень хороши. Ваши примеры обеспечивают большую сложность для чего-то, что является простой идеей. Они не иллюстрируют проблему вопроса.
Адам Цукерман
Конструктор DI передает параметр. За исключением того, что вы передаете объект интерфейса, а не конкретный объект класса. Именно эта «зависимость» от конкретной реализации решается с помощью DI. Конструктор не знает, какой тип ему передается, и не заботится, он просто знает, что он получает тип, который реализует интерфейс. Я предлагаю PluralSight, у него есть отличный курс DI / IOC для начинающих.
Хардграф