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

В функциональном программировании смысл DI заключается в передаче функциям других функций.
Вот — пример функции (Erlang):
Работает она очень просто: получает число и прибавляет к нему единицу.
Внедрим эту зависимость в функцию, которая перебирает список чисел и применяет переданную ей функцию к каждому из его элементов:
Замечательно! Конструкция
А как насчёт Lisp?
Работает! А JavaScript?
Прекрасно!
Теперь нам остаётся лишь переименовать
Если говорить о Haskell, то можно сказать, что благодаря усилиям замечательных разработчиков GHC (Glasgow Haskell Compiler), компилятора языка Haskell из Глазго, это было реализовано в .NET, в форме Language Integrated Query (LINQ):
Соответствующий метод назвали
Итак, всякий раз, когда я слышал о DI, я думал, что всё то, о чём я рассказал, и есть внедрение зависимостей. Но, как оказалось, это не так.
А это, да простит нас Алан Кэй, что ещё такое?
Я понимаю, что выглядит это несколько несерьёзно, но то, что можно счесть недоработками, появилось тут лишь из-за того, что я стремился сделать этот пример как можно короче. Нормальный код занял бы столько же места, сколько занимает вся эта статья.
Вот что можно сказать в пользу такого стиля программирования:
В этом есть что-то волшебное, черпающее свою силу из глубин нашей программы:
Я, чтобы никого не перегружать ненужным чтением, опустил тут XML-код.
Разобраться с каждым отдельным компонентом может быть и несложно, но — ценой необходимости постоянно просматривать код и добавлять реализацию в место, предшествующее ему. Это — как если взять Lisp-код, помещающийся на одном экране, и разбросать его по нескольким файлам.
В деле исследования и написания кода вам поможет ваш новый лучший друг — команда
Полагаю, что это не особо сильно связано с внедрением зависимостей.
У меня такое ощущение, что, решая пользоваться подобным внедрением зависимостей в корпоративной разработке, мы получаем не только банан, который был нам нужен, но ещё и гориллу, которая держит этот банан, а заодно и все джунгли. А раз уж речь зашла о бананах, то недалеко и до ядовитых лягушек-древолазов.
Нет ничего опасного в том, чтобы вручную создавать необходимую инфраструктуру:
Такой код легко читать, его легко писать и понимать. В нём не нужны зависимости. Если нужно в него что-то добавить — делается это прямо в нём самом. Не нужно ничего регистрировать, не нужно использовать XML-файлы. Это — просто код. Ваш код.
Если вам необходимо более абстрактное внедрение зависимостей, в этом деле вам поможет совершенно замечательный инструмент — интерфейсы. У передачи
Если вам повезло, и вы пишете код в функциональном стиле, то передача функций другим функциям делается ещё проще. Иногда благодаря этому в нашем распоряжении оказываются замечательные механизмы обеспечения безопасности во время компиляции кода.
Применяйте композицию для создания более продвинутых функций. Пусть данные будут данными.
И передавайте своим функциям какие-нибудь значения!

Мне нравится применять внедрение зависимостей (передавать функциям значения)
Как вы относитесь к внедрению зависимостей?


В функциональном программировании смысл DI заключается в передаче функциям других функций.
Вот — пример функции (Erlang):
-module(example). -export(add_one/1). add_one(N) -> N + 1
Работает она очень просто: получает число и прибавляет к нему единицу.
Внедрим эту зависимость в функцию, которая перебирает список чисел и применяет переданную ей функцию к каждому из его элементов:
Eshell V12.0 (abort with ^G) 1> c(examples). {ok,examples} 2> lists:map(examples:add_one, [0, 1, 2, 3]). [1, 2, 3, 4]
Замечательно! Конструкция
lists:map проходится по списку и применяет к каждому числу из списка функцию add_one.А как насчёт Lisp?
* (DEFINE ADD-ONE (N) (+ N 1)) ADD-ONE * (MAPCAR #'ADD-ONE '(0 1 2 3)) (1 2 3 4)
Работает! А JavaScript?
> const addOne = n => n + 1 undefined > [0, 1, 2, 3].map(addOne) (4) [1, 2, 3, 4]
Прекрасно!
Теперь нам остаётся лишь переименовать
map в fmap, притворившись, что мы понимаем, что такое «моноидная операция». И вот — мы уже стали Haskell-программистами.Если говорить о Haskell, то можно сказать, что благодаря усилиям замечательных разработчиков GHC (Glasgow Haskell Compiler), компилятора языка Haskell из Глазго, это было реализовано в .NET, в форме Language Integrated Query (LINQ):
using System.Linq; using System.Collections.Generic; public static int AddOne(int n) => n + 1; new List<int>(){0, 1, 2, 3} .Select(AddOne); // [1, 2, 3, 4]
Соответствующий метод назвали
Select для того чтобы никто ничего не заподозрил, намекая на то, что это — всего лишь типизированный язык SQL, а не функциональное программирование. Хитрецы.Итак, всякий раз, когда я слышал о DI, я думал, что всё то, о чём я рассказал, и есть внедрение зависимостей. Но, как оказалось, это не так.
Готовьтесь! Сейчас начнётся!
public interface IGetAThing { IThing GetThing(); } public MyThingGetter : IGetAThing { private readonly IThingFactory _factory; public MyThingGetter(IThingFactory factory) { _factory = factory; } public IThing GetThing() { return _factory.Get(thing.NORMAL); } } public MyApi { private readonly IGetAThing _myThingGetter; public MyApi(IGetAThing thing) { _myThingGetter = thing; } public IThing GetThing() { return _myThingGetter.GetThing(); } }
А это, да простит нас Алан Кэй, что ещё такое?
Я понимаю, что выглядит это несколько несерьёзно, но то, что можно счесть недоработками, появилось тут лишь из-за того, что я стремился сделать этот пример как можно короче. Нормальный код занял бы столько же места, сколько занимает вся эта статья.
Вот что можно сказать в пользу такого стиля программирования:
- Каждая зависимость внедрена в код (за исключением перечисления).
- Мы успешно разделили программу на аккуратные SOLID-блоки.
MyAPIдаёт интерфейс, рассчитанный на определённого пользователя этого интерфейса, не предоставляя сведений о внутренней реализации интерфейса.MyThingGetterдаёт интерфейс для получения объектаThing, но делегирует выполнение этой операции сущностиFactory, которая подключается к программе во время её выполнения.Factoryпринимает элемент перечисления, что позволяет предотвратить ошибки, связанные с «магическими» строками.- Любой слой программы можно заменить, не трогая при этом слои, находящиеся выше или ниже его.
В этом есть что-то волшебное, черпающее свою силу из глубин нашей программы:
static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureServices((_, services) => services.AddHostedService<Program>() .AddScoped<IThingFactory, ThingFactory>() .AddScoped<IGetAThing, MyThingGetter>());
Я, чтобы никого не перегружать ненужным чтением, опустил тут XML-код.
Достоинства
- Чёткое разделение слоёв абстракции.
- Заменяемые, благодаря паттерну Стратегия, компоненты.
- При таком подходе редактировать существующий код нужно не так часто, как при использовании других подходов.
Недостатки
- Смерть от тысяч классов.
- Раздолье для сборщика мусора.
- Необходимость тестирования монструозных конструкций.
- Царство существительных.
- «Никто больше не покупает молотки».
Разобраться с каждым отдельным компонентом может быть и несложно, но — ценой необходимости постоянно просматривать код и добавлять реализацию в место, предшествующее ему. Это — как если взять Lisp-код, помещающийся на одном экране, и разбросать его по нескольким файлам.
В деле исследования и написания кода вам поможет ваш новый лучший друг — команда
Перейти к определению (F12), а вот тестирование — это то, в чём у вас помощников не будет. Задача усложняется.IMyThingGetter _myThingGetter; public static void TearMeUp() { _myThingGetter = new Mock<MyThingGetter>().When(MyThing.GetThing).Do((ThingType t) => { t == thing.NORMAL ? new Thing() : throw new ArgumentExceptionError(); } } [MakeThisTestRunPlease(true)] public static void Test_MyThingGetter_Should_GetAThing_When_WeWantTo() { // Приведи в порядок. TearMeUp(); // Действуй. var thing = _myThingGetter.GetThing(thing.NORMAL); // Купи мою книгу. assert.Equal(thing, new Thing()); WakeMeUpInside(); } public static void WakeMeUpInside() { _myThingGetter = null; }
Какое это имеет отношение к внедрению зависимостей?
Полагаю, что это не особо сильно связано с внедрением зависимостей.
У меня такое ощущение, что, решая пользоваться подобным внедрением зависимостей в корпоративной разработке, мы получаем не только банан, который был нам нужен, но ещё и гориллу, которая держит этот банан, а заодно и все джунгли. А раз уж речь зашла о бананах, то недалеко и до ядовитых лягушек-древолазов.
Нет ничего опасного в том, чтобы вручную создавать необходимую инфраструктуру:
logger := log.New(log.DefaultConfig{}) dbConfig := db.NewConfig{ Logger: logger, } db := db.New(dbConfig) myApi := &myApi{ Logger: logger, DB: db, }
Такой код легко читать, его легко писать и понимать. В нём не нужны зависимости. Если нужно в него что-то добавить — делается это прямо в нём самом. Не нужно ничего регистрировать, не нужно использовать XML-файлы. Это — просто код. Ваш код.
Если вам необходимо более абстрактное внедрение зависимостей, в этом деле вам поможет совершенно замечательный инструмент — интерфейсы. У передачи
Reader структуре в виде зависимости могут найтись варианты применения, но прямая передача Reader функции создаст вам гораздо меньше проблем, особенно — при тестировании.Если вам повезло, и вы пишете код в функциональном стиле, то передача функций другим функциям делается ещё проще. Иногда благодаря этому в нашем распоряжении оказываются замечательные механизмы обеспечения безопасности во время компиляции кода.
Применяйте композицию для создания более продвинутых функций. Пусть данные будут данными.
И передавайте своим функциям какие-нибудь значения!

Мне нравится применять внедрение зависимостей (передавать функциям значения)
Как вы относитесь к внедрению зависимостей?

