Механизм внедрения зависимостей (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
функции создаст вам гораздо меньше проблем, особенно — при тестировании.Если вам повезло, и вы пишете код в функциональном стиле, то передача функций другим функциям делается ещё проще. Иногда благодаря этому в нашем распоряжении оказываются замечательные механизмы обеспечения безопасности во время компиляции кода.
Применяйте композицию для создания более продвинутых функций. Пусть данные будут данными.
И передавайте своим функциям какие-нибудь значения!
Мне нравится применять внедрение зависимостей (передавать функциям значения)
Как вы относитесь к внедрению зависимостей?
Комментарии (6)
mvv-rus
14.11.2021 22:49Применяйте композицию для создания более продвинутых функций.
Хорошо сказано. Но как это сделать — если речь идет о C# и ASP.NET?
Я знаю только один способ: ручками — писать по каждому случаю свой делегат.
dyadyaSerezha
Считаю, что как и всё более-менее модное, внедрение зависимостей сильно переиспользовано сейчас. Как правильно заметил автор, использование десятков, если не сотен фабрик и интерфейсов на пустом месте сильно перегружает код. А уж когда это всё сверху еще и управляется через XML, становится совсем грустно. Простейшее действие превращается в десятки строк кода, расбросанных по некольким файлам и ничего не добавляет программе, кроме геморроя разработчикам. То есть, для каждого инструмента есть свои, подходящие условия его использования. Сейчас же DI используется повсеместно и вообще для всего. Что печально, конечно.
MFilonen2
А что же Вы предлагаете взамен?
Или Вы критикуете только реализацию через фреймворк, а передачу зависимостей в конструкторе / переменными DI уже не считаете?
Или же Вам нравится управление глобальным состоянием отовсюду и миллион синглтонов?
nin-jin
Я бы предложил контексты: https://habhub.hyoo.ru/#!author=nin-jin/repo=HabHub/article=40
dyadyaSerezha
Я не предлагаю что-то конкретное, но предлагаю известный KISS-принцип. Keep it simple, stupid.