Страницы

Поиск по вопросам

понедельник, 25 ноября 2019 г.

Зачем нужен Dependency Injection контейнер?


Принцип Inversion of Control мне понятен и логичен, но зачем нужен DI я осознать не могу.
Пример:

public class Samurai 
{
    public IWeapon Weapon { get; private set; }
    public Samurai(IWeapon weapon) 
    {
        this.Weapon = weapon;
    }
}


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

this.Bind().To();


Окей, все хорошо, НО зачем нам нужен DI фреймворк, если мы можем сделать по сут
тоже самое:

var samurai = new Samurai(new Sword());


Т.е. в конструктор мы точно так же, как и в примере с DI фреймворком можем подменить реализацию, изменив всего-лишь одно слово в одном месте.
Так в чем же тогда смысл DI контейнера? Извиняюсь, если вопрос глупый, я новичок.
    


Ответы

Ответ 1



Inversion Of Control - это принцип используемый для уменьшения связанности кода. Dependency Injection - это один из способов реализации данного принципа в которо зависимости передаются через конструктор (как правило) или через установку свойств (реже). Ninject, Unity и т.п. это Dependency Injection Container - инструмент для более простог применения Dependency Injection. Как правило он позволяет более удобно задавать композицию объектов, их время жизни, а так же перехват (interception). Для применения Dependency Injection использование каких-либо фреймворков (контейнеров) не обязательно. Они лишь позволяют сделать это более удобно. Чтобы было лучше понятно я попробую (совсем вкратце) описать преимущества DI-контейнера. В указанном выше примере все достаточно просто. Существует всего несколько классо которые легко можно вручную скомпоновать вместе. В таком случае смысл в DI-контейнере действительно отсутствует и все можно сделать руками достаточно просто. Но, когда проект становится больше, появляется больше объектов и их вложенность дру в друга становится глубже - управлять всем этим вручную становится достаточно сложно. Потому, что это будет требовать или написания большого объема кода либо собственного, встроенного фреймворка. Контейнер, в свою очередь, позволяет: Задавать различную конфигурацию и удобно ей управлять. Использовать различные соглашения чтобы контейнер сам "подцеплял" нужные ему типы найденные по определенным условиям в проекте. Если конфигурация читается из внешнего файла (например xml), то поведение программы можно поменять без перекомпиляции. И т.д. и т.п. Так как в сложных случаях может быть так, что объекты которые передаются как зависимост так же могут требовать для себя какие-то зависимости которые так же могут требовать для себя какие зависимости и т.д. реализовывать все это вручную будет трудоемко. Контейнер же с легкостью сделает это все самостоятельно. Позволят задавать время жизни объектов. Рассмотрим простой пример: public class A { public A(IB b, IC c) {} } public interface IB {} public class B { public B(IC c) {} } public interface IC {} public class C {} В данном случае, при создании экземпляра типа А нам требуется экземпляр типа IB IC, IB в свой очередь требует так же экземлпяр типа IС. Нам может потребоваться ил создать в каждом случае новый экземпляр типа IC или исползовать в обоих случаях один и тот же. Контейнер позволяет легко этим манипулировать. Помимо этих двух случаев бывают еще и другие. Позволяет применять перехват (interception). Это возможность с помощью контейнер встраивать какой-нибудь полезный код (типа логирования, проверки прав и т.п.) перед\после вызовов методов интерфейса зависимости. Об этом лучше почитать отдельно, с примерами, для конкретного контейнера. У Ninject для этого есть специальное расширение.

Ответ 2



Вот так может выглядеть внедрение зависимостей в реальном мире: Розетка это ваш самурай, вилка - оружие. Между розеткой и вилкой есть интерфейс который определяется габаритами штекера и напряжением сети. Может быть тысяча розеток и тысяча вилок, которые реализуют этот интерфейс и они будут совместимы между собой в любых комбинацих. Когда вы внедряете зависимость через конструктор, вы создаете нечто аналогичное вилке с розеткой. Далее. Работу над вашей программой можно (очень условно) разбить на две части: Определение интерфейсов между компонентами и реализация этих компонентов (вы продумываете интерфейсы между вилками и розетками и создаете объекты, реализующие эти интерфейсы) Сборка компонентов в работающую программу (вы садитесь и подключаете все между собой в определенном порядке) Если вы сделаете так: var samurai = new Samurai(new Sword()); то сборка будет размазана по всему вашему коду, у вас будут розетки, которые сами будут подключать к себе компоненты, это равносильно вот этому: Когда вы используете контейнер внедрения зависимостей, вы выполняете сборку компоненто вне самих компонентов в одном месте, которое называется корень компоновки. Это то место, где вы пишете вот это: this.Bind().To(); В этом коде просто указывается какой объект надо использовать, когда требуется интерфей IWeapon. В реальном приложении конфигурация будет сложнее. В ней будут инструкции, указывающие, что некоторые классы должны быть синглтонами, инструкции описывающие применение декораторов в вашем проекте. Вообще же DI это одна из тех концепций, которые только кажутся очень простыми и которы очень легко использовать неправильно. Точнее она действительно достаточно проста, но есть неочиведные нюансы, которые решают все и непонимание которых может обесценить ваш опыт ее применения, превратив его в культ Карго. Поэтому очень рекомендую прочитать по этой теме как можно больше, одного ответа на so здесь явно недостаточно.

Ответ 3



Окей, все хорошо, НО зачем нам нужен DI и дополнительные фреймворки, если мы можем сделать по сути тоже самое: var samurai = new Samurai(new Sword()); Это и есть Dependency Injection. Даже не используя никаких фреймворков, вы всё равн в этом примере использовали DI. Вы подали классу Samurai его зависимость при помощи инъекции этой зависимости в конструктор. DI рекомендуется использовать всегда, вне зависимости от того, пользуетесь ли в готовыми библиотеками или нет. Польза состоит в том, что классы, у которых есть зависимости (Samurai), не будут знать, где и каким образом эти зависимости получить. Они просто декларативно скажут: меня без этой зависимости (IWeapon) использовать нельзя. Использование DI, даже без контейнеров, значительно уменьшает связанность кода. ваших классов пропадают знания об инфрастуктуре вашего приложения и они становятся более независимыми, подчиняющимися принципам SOLID, тестируемыми и поддерживаемыми. Контр-пример А теперь представьте, что вы не используете DI. Вместо этого используется какая-то разновидность антипаттерна Service Locator (я его здесь назвал ConfigurationService): public class Samurai { public IWeapon Weapon { get; private set; } public Samurai() { this.Weapon = ConfigurationService.Get(); } } Теперь, если вы в будущем решите применить IoC-контейнер, или же подавать зависимост в класс не через ConfigurationService, а каким-то другим способом, то вам придётся изменять класс Samurai. А что, если у вас в приложении десятки, сотни классов, которые используют этот Service Locator? Придётся изменять их все. Когда используется паттерн Dependency Injection (как в вашем изначальном примере) при добавлении IoC-контейнера или изменения способа создания/получения зависимостей сами классы, которые используют зависимости, изменять не придётся. В этом состоит огромная польза DI. Классы, использующие этот паттерн, не знают и не хотят знать об инфраструктуре приложения. Засчёт этого они более универсальны.

Ответ 4



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

Комментариев нет:

Отправить комментарий