Механизм внедрения зависимостей (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<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)


  1. dyadyaSerezha
    14.11.2021 17:58
    +2

    Считаю, что как и всё более-менее модное, внедрение зависимостей сильно переиспользовано сейчас. Как правильно заметил автор, использование десятков, если не сотен фабрик и интерфейсов на пустом месте сильно перегружает код. А уж когда это всё сверху еще и управляется через XML, становится совсем грустно. Простейшее действие превращается в десятки строк кода, расбросанных по некольким файлам и ничего не добавляет программе, кроме геморроя разработчикам. То есть, для каждого инструмента есть свои, подходящие условия его использования. Сейчас же DI используется повсеместно и вообще для всего. Что печально, конечно.


    1. MFilonen2
      14.11.2021 18:54
      -1

      А что же Вы предлагаете взамен?
      Или Вы критикуете только реализацию через фреймворк, а передачу зависимостей в конструкторе / переменными DI уже не считаете?
      Или же Вам нравится управление глобальным состоянием отовсюду и миллион синглтонов?


      1. nin-jin
        14.11.2021 21:24

        Я бы предложил контексты: https://habhub.hyoo.ru/#!author=nin-jin/repo=HabHub/article=40


      1. dyadyaSerezha
        16.11.2021 03:27

        Я не предлагаю что-то конкретное, но предлагаю известный KISS-принцип. Keep it simple, stupid.


  1. mvv-rus
    14.11.2021 22:49

    Применяйте композицию для создания более продвинутых функций.

    Хорошо сказано. Но как это сделать — если речь идет о C# и ASP.NET?
    Я знаю только один способ: ручками — писать по каждому случаю свой делегат.


  1. marlishink
    15.11.2021 11:19

    Отличная статья, спасибо большое, жаль мало таких статей на Хабре.