[Перевод] Printf("%s %s", внедрение, зависимостей)
Механизм внедрения зависимостей (Dependency Injection, DI) стал одним из тех аспектов корпоративного программирования, с которыми мне было сложнее всего разобраться. А именно, дело было в том, что это понятие уже имело для меня смысл. Мне, для того, чтобы этот смысл увидеть, не пришлось много всего читать.
В функциональном программировании смысл 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(){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()
.AddScoped()
.AddScoped());
Я, чтобы никого не перегружать ненужным чтением, опустил тут XML-код.
Достоинства
- Чёткое разделение слоёв абстракции.
- Заменяемые, благодаря паттерну Стратегия, компоненты.
- При таком подходе редактировать существующий код нужно не так часто, как при использовании других подходов.
Недостатки
Разобраться с каждым отдельным компонентом может быть и несложно, но — ценой необходимости постоянно просматривать код и добавлять реализацию в место, предшествующее ему. Это — как если взять Lisp-код, помещающийся на одном экране, и разбросать его по нескольким файлам.
В деле исследования и написания кода вам поможет ваш новый лучший друг — команда Перейти к определению (F12)
, а вот тестирование — это то, в чём у вас помощников не будет. Задача усложняется.
IMyThingGetter _myThingGetter;
public static void TearMeUp()
{
_myThingGetter = new Mock().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
функции создаст вам гораздо меньше проблем, особенно — при тестировании.
Если вам повезло, и вы пишете код в функциональном стиле, то передача функций другим функциям делается ещё проще. Иногда благодаря этому в нашем распоряжении оказываются замечательные механизмы обеспечения безопасности во время компиляции кода.
Применяйте композицию для создания более продвинутых функций. Пусть данные будут данными.
И передавайте своим функциям какие-нибудь значения!
Мне нравится применять внедрение зависимостей (передавать функциям значения)
Как вы относитесь к внедрению зависимостей?