[Перевод] Printf("%s %s", внедрение, зависимостей)

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

image-loader.svg

В функциональном программировании смысл 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 функции создаст вам гораздо меньше проблем, особенно — при тестировании.

Если вам повезло, и вы пишете код в функциональном стиле, то передача функций другим функциям делается ещё проще. Иногда благодаря этому в нашем распоряжении оказываются замечательные механизмы обеспечения безопасности во время компиляции кода.

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

И передавайте своим функциям какие-нибудь значения!

image-loader.svg


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

Как вы относитесь к внедрению зависимостей?

image-loader.svg

© Habrahabr.ru