Как стать автором
Обновить

Комментарии 23

А в чем value-add если сравнивать с обычным new? У вас так же присутствует условное ветвление. Для изменения реализации вам все еще нужно пересобирать и переставлять приложение. Сомневаюсь, что в обозримом будующем у вас изменится поведение для "local" и тем более для "cloud".


Может быть я чего-то не понимаю. Подскажите зачем?

С удовольствием подскажу :)

Смотрите:
А в чем value-add если сравнивать с обычным new?

этот пример показывает только механизм внедрения по идентификатору, поэтому используются очень простые классы, только для примера. Но допустим, в сервисах у нас в конструктор будут внедряется другие зависимости (сериализатор, низкоуровневый сервис для работы с файловой системой — дисковой или облачной, доступ к базе данных и т.д.). Если я буду использовать new, чтобы создать класс сервиса, мне придётся создать вручную и все зависимости, а так все сделает инъектор зависимостей, исходя из настроек IoC-контейнера. И нельзя не упомянуть об удобстве тестирования таких классов.

Для изменения реализации вам все еще нужно пересобирать и переставлять приложение.

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

Сомневаюсь, что в обозримом будующем у вас изменится поведение для «local» и тем более для «cloud».

Поведение может не измениться, но может измениться технология. Например работали с Amazon и решили перейти на Azure. Веб приложение останется без изменений, т.к. реализации сервисов (и настройка контейнера) находятся в отдельной сборке.

Надеюсь, я правильно понял Ваши вопросы. Если что-нибудь остается непонятным, спрашивайте — с удовольствием отвечу :)
вот чем лучше: public LocalController([Dependency(«local»)] IService service)
по сравнению с public LocalController(LocalService service) ??

В первом случае мы закладываемся на какую то магическую строку, по которой обязательно должен быть создал локальный сервис(не просто так же в атрибуте строка «local» сидит). Во втором случае — такой зависимости нет. В чём прикол?
LocalService это конкретный тип, а IService со строкой это абстракция

В чём прикол?


Не стоит рассматривать эти классы, как решение, которое выполняет что-то конкретное. Это демонстрация архитектуры приложения, а не определенного функционала.

public LocalController([Dependency(«local»)] IService service) 

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

Например, у нас приложение рисует графики. Есть такой контроллер
ChartsController
public class ChartsController : Controller
{
    public IActionResult PieChart([FromServices("pie")] IChartService chartService)
    {
        return this.View(chartService.GetData());
    }

    public IActionResult BarChart([FromServices("bar")] IChartService chartService)
    {
        return this.View(chartService.GetData());
    }

    public IActionResult LineChart([FromServices("line")] IChartService chartService)
    {
        return this.View(chartService.GetData());
    }
}



Допустим, мы решили использовать Google Charts, и для каждого графика будет реализован свой сервис. Эти сервисы мы кладем в сборку Charts.dll и ссылаемся на нее из веб приложения
GooglePieChartService
GoogleBarChartService
GoogleLineChartService

Затем мы решили поменять фреймворк и использовать Chart.js. Мы реализовали сервисы
ChartJsPieChartService
ChartJsBarChartService
ChartJsLineChartService

поместили их в сборку Charts.dll и т.о. у нас уже новая реализация.

Класс ChartsController при этом остался без изменений. Если бы мы указали зависимости на GoogleXYZChartService, то пришлось бы там все менять. А теперь представьте, что у вас будет не одна зависимость, а, скажем, штук 5. И каждый тип будет в свою очередь иметь свои зависимости. Если везде использовать конкретные типы вместо интерфейсов, может полдня уйти только на изменения типов.
Хм, я бы просто сделал PieChartService/BarChartService/LineChartService. Реализовал бы из используя Google Charts. Решил бы поменять фреймвок — ну поменял бы реализацию этих классов.
В этом случае вы просто задаете асбстракцию типом класса вместо интерфейса.

Конечно, абстракцию можно задать и классом, иногда даже не абстрактным (тот же DbContext).
Вопрос выбора интерфейса или абстрактного класса, это уже другая история.

я бы просто сделал PieChartService/BarChartService/LineChartService

В подходе, который Вы предлагаете, конкретная реализация уже задана, а в статье идет речь о множественной реализации интерфейса, когда есть один тип абстракции и несколько реализаций, а в процессе выполнения выбирается нужный. Опять же, для чего это нужно, отдельный вопрос, в каких-то случаях это может быть более удобно и позволит сократить количество кода.

Например, мы можем изменить контроллер
ChartsController
public class ChartsController : Controller
{
    public IActionResult GetChart([FromServices] IServiceProvider services, string chartType)
    {
        var chartService = services.Resolve<IService>(chartType);
        return this.View(chartService?.GetData());
    }
}



Если бы у нас не было общей абстракции, пришлось бы писать логику выбора
ChartsController
public class ChartsController : Controller
{
    public IActionResult GetChart([FromServices] IServiceProvider services, string chartType)
    {
        if (chartType == "bar")
        {
            var chartService = services.Resolve<BarChartService>();
            return this.View(chartService.GetData());
        } 
        else if (...) {}
        else if (...) {}
        else {}
    }
}


или иметь action под каждый тип сервиса, как это было в предыдущем примере

В первом случае мы закладываемся на какую то магическую строку, по которой обязательно должен быть создал локальный сервис(не просто так же в атрибуте строка «local» сидит).

Эта магическая строка — это тоже абстракция, которая указывает, что должен быть создан некий абстрактный локальный сервис, но тип этого сервиса не известен на этом этапе

вот чем лучше: public LocalController([Dependency(«local»)] IService service)
по сравнению с public LocalController(LocalService service) ??

Чтобы расставить точки над i :) В статье не говорится о том, что лучше использовать [Dependency(«local»)] IService service вместо конкретного типа. Этот выбор остается на усмотрение разработчика, что ему больше подходит. Суть статьи — показать, как реализовать [FromSerives(«local»)] или Resolve(«local»), потому что этого функционала нет в «коробке».
Если я буду использовать new, чтобы создать класс сервиса, мне придётся создать вручную и все зависимости, а так все
сделает инъектор зависимостей, исходя из настроек IoC-контейнера.

Ну я знаком с IoC и DI. И насмотрелся на множество ситуаций, когда, как мне кажется, использование IoC конейнеров нисколько не помогает, а вот мешает существенно. Ваш код, как я понял — это демонстрация возможностей, но я собственно о том и пишу. Использование продемонстрированых техник для описаной задачи выглядит как стрельба из пушки по воробьям.


Реализация может находиться в отдельной сборке, поэтому для ее изменения мне достаточно будет поменять сборку в
папке с билдом, не пересобирая все приложение.

Собственно в чем плюс возможности пересборки одной библиотеки? Это DI конфигурация. Как минимум после ее изменения нужно прогнать полный кейс автоматизированых тестов, а как максимум произвести регрессионное тестирования. Изменяя эту конфигурацию — вы меняете поведение всей системы. А учитывая сценарии вроде описанного вами — вы еще и можете изменить поведение в случайных местах (вы решаете какую реализацию использовать не из DI контейнера, а ветвлением).


В общем честно. Я слышал этот аргумент много раз, не видел ни разу чтобы на более менее подросшем проекте когда-нибудь меняли конфигурацию DI контейнера без доп. проблем. Этот аргумент мне напоминает довод за ORM систему — типа мы можем поменять базу данных не меняя кода приложения — теоритически да, практически — нет.


Кроме того, можно создать ещё один уровень абстракции, если вынести настройку контейнера в отдельную сборку. Т.о. веб
приложение вообще не будет завязано на конкретные реализации, а типы сервисов станут доступны только во время
выполнения.

Вы себе представляете как потом это тестировать? А воспроизводить баги?


Поведение может не измениться, но может измениться технология. Например работали с Amazon и решили перейти на
Azure.

Ага. И после перехода вам не надо будет все тестировать, переписывать запросы для оптимизации?! Помните принцип YAGNI? Вы внедряете весьма тяжелую зависимость ради гипотетической возможности. В случае если вы пишете enterprise — такие переходы маловероятны. В случае если вы пишете high-load — использование DI/IoC контейнера — это серьезная растрата производительности, а значит денег клиента.


Мало того адекватное использование DI/IoC контейнера в приложении — это когда DI/IoC конейнер можно поменять или вообще убрать не переписывая практически все приложение. Что я вижу в вашем случае — вы используете ServiceLocator паттерн — а это вообще жесть. Он в общем для другого.

использование IoC конейнеров нисколько не помогает, а вот мешает существенно

не могу согласиться :)

Использование продемонстрированых техник для описаной задачи выглядит как стрельба из пушки по воробьям.
А учитывая сценарии вроде описанного вами
Что я вижу в вашем случае — вы используете ServiceLocator паттерн — а это вообще жесть. Он в общем для другого.

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

Вы себе представляете как потом это тестировать? А воспроизводить баги?

Конечно представляю. А в чем по-вашему проблема?

Ага. И после перехода вам не надо будет все тестировать, переписывать запросы для оптимизации?!
В случае если вы пишете enterprise — такие переходы маловероятны. В случае если вы пишете high-load — использование DI/IoC контейнера — это серьезная растрата производительности, а значит денег клиента.

Это вопрос требований. И принятие решений об архитектуре приложения строится на основе этих требований. Если использование DI/IoC не оправдано или не удовлетворяет требуемой производительности, его не используют. Суть статьи не в том, чтобы сказать «Все используйте IoC!», а в том, чтобы показать, как реализовать знакомый по другим IoC-фреймовркам функционал внедрения по идентификатору. Я же не заставляю всех это использовать везде, где бы то ни было :)
не могу согласиться :)

Не можете согласиться с тем, что во многих ситуациях IoC и DI неуместны? Перефразируя можно ли вас понять так что: "IoC и DI — всегда уместны" или "IoC и DI чаще всего уместны"?


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

Может быть реализация динамических плагинов?


Конечно представляю. А в чем по-вашему проблема?

В количестве тест кейсов. Любые динамические конфигурации требуют тестирования всех комбинаций. Ручное тестирование в таком случае сразу отсекаем, но даже написание автоматических тестов все против всех — это суровый такой труд.


Может быть вы представляете себе это по другому? Как по-вашему надо подходить к вопросу?


Суть статьи не в том, чтобы сказать «Все используйте IoC!», а в том, чтобы показать, как реализовать знакомый по другим IoC-
фреймовркам функционал внедрения по идентификатору.

Ну т.е. вы реализовали на IoC конейнере паттерн Factory. Очень хорошо.


Я же не заставляю всех это использовать везде, где бы то ни было :)

И это замечательно.

Не можете согласиться с тем, что во многих ситуациях IoC и DI неуместны? Перефразируя можно ли вас понять так что: «IoC и DI — всегда уместны» или «IoC и DI чаще всего уместны»?

Я согласен с тем, что бывают ситуации, когда использование DI неуместно. Это индивидуально для каждого проекта.

Может быть реализация динамических плагинов?

Хорошая идея, спасибо большое! Попробую :)

Любые динамические конфигурации требуют тестирования всех комбинаций.
Может быть вы представляете себе это по другому? Как по-вашему надо подходить к вопросу?

Я говорю не о динамической замене одной сборки на другую в ходе выполнения. Динамическая конфигурация в данном случае происходит на этапе запуска приложения. Речь об изменении поведения, если изменятся требования. Достаточно поменять код в одной сборке, а другая останется без изменений.

Ну т.е. вы реализовали на IoC конейнере паттерн Factory. Очень хорошо.

А плохого что?)) И это не Factory, скорее Service Locator. Во-первых, Factory подразумевает создание однотипных объектов. Во-вторых, Factory создает объекты, в то время как Service Locator не обязательно будет их создавать сам. IoC-контейнер может реализовывать паттерн Service Locator, при необходимости. Поэтому его можно спутать с паттерном Factory.
Я говорю не о динамической замене одной сборки на другую в ходе выполнения.

И я о них же. Зависит, конечно, от того, как вы поставляете софт. Если кастомные установки, с доводкой под клиента, то тестировать нужно все возможные конфигурации. Если же вы продаете сервис и набор конфигураций заранее известен, то смысла делать их динамическими нет. Принцип KISS.


IoC-контейнер может реализовывать паттерн Service Locator, при необходимости.

Вообще существует мнение, что ServiceLocator — это антипаттерн.

Вообще существует мнение, что ServiceLocator — это антипаттерн.

Я с этим мнением согласен. Имхо, антипаттерном он является тогда, когда метод Resolve вызывается многократно при каждом удобном случае. Но если он разумно используется для реализации DI и при этом выполняет свою функцию, то что в этом плохого? Какие есть варианты получше?
В том и дело, что если использовать сервис локатор правильно, то он будет неотличим от контейнера(Не будет зависимостей от локатора). В свою очередь практически все контейнеры позволяют использовать себя как сервис локаторы.

Кстати, я так и не нашел решения как обходится без сервис локатора(Резолва вне композиции объектов), когда нужны фабрики и отсутствует возможность автоматической генерации.

Регистрировать Func и т.п. мне кажется крайне уродливым.
Абсолютно согласен, плюсую :)

Для реализации DI контейнера пожалуй других вариантов и нет (нет ну можно конечно конструировать объекты через рефлексию, или там извратиться с Func или пачку фабрик понасоздавать; но будет еще кривее).


А вот для реализации DI самого по себе — ничего не надо. Либо DI контейнер с отдельной конфигурацией, когда надо динамически менять поведение — либо new, когда не надо.

Ага, а потом попробуйте добавить например кеш на какой-нибудь сервис. И в итоге ваш сервис будет заботится о своей непосредственной работе, а так же о кеше. Это явно дурно пахнущий код.

С Ди это решается на раз с помощью декоратора или даже перехвата. А если же вы захотите использовать декоратор, то вам придется проштудировать весь исходный код в поисках new. Так же вам нужно будет шерстить весь код, если классу понадобилась новая зависимость. Кстати, эту зависимость еще надо пробросить до сервиса. А как обстоят дела с временем жизни зависимостей? При new вам придется кропотливо за всем следить и управлять этим руками.
Ага, а потом попробуйте добавить например кеш на какой-нибудь сервис. И в итоге ваш сервис будет заботится о своей
непосредственной работе, а так же о кеше. Это явно дурно пахнущий код.

Вы про то, как например в Java сделана BufferedStream? Что же в нем такого уж дурно пахнущего? Опять же если вы кэш делаете, то видимо и инвалидацию — она у вас ведь не по таймауту?


С Ди это решается на раз с помощью декоратора или даже перехвата

Видел такие реализации. Кошмар дикий. Либо начинается протечка абстракции (abstraction leak) либо правила инвалидации настолько дубовые, что польза от кэша становится сомнительной.


Так же вам нужно будет шерстить весь код, если классу понадобилась новая зависимость.

А можете поподробнее? Как это вы так структурируете код, что добавление зависимости требует что-то шерстить?


А как обстоят дела с временем жизни зависимостей? При new вам придется кропотливо за всем следить и управлять
этим руками.

А как обстоят дела с наследованием и несколькими реализациями интерфейса? Вы же эти правила прописываете в кофигурации контейнера? А значит вам придется кропотливо следить за тем в каком случае кто и как решил отрезолвить интерфейс (в одном случае нам тот же кэш нужен, а в другом нет или другой кэш). Конфигурация видимо инициализируется на запуске приложения (привет многопоточность), либо на запуске каждого инстанса (привет производительность).


Ну а если у вас строго один класс — один интерфейс, то это как раз тот самый code smell.

Вы про то, как например в Java сделана BufferedStream? Что же в нем такого уж дурно пахнущего? Опять же если вы кэш делаете, то видимо и инвалидацию — она у вас ведь не по таймауту?

Инвалидацией занимается тотже декоратор. Дурной запах в том, что при добавлении кеша сервис начинает выполнять две задачи: свою работу и заботу о кеше.
А можете поподробнее? Как это вы так структурируете код, что добавление зависимости требует что-то шерстить?

Например сервису «карты» понадобилось отправлять сообщения пользователям при каких-то событиях. Если мы используем ДИ, то мы просто добавляем в конструктор параметр IMessageCenter. А если мы используем new, то надо искать везде этот new и добавлять туда новый параметр, к тому же надо что бы вызывающий метод имел доступ к IMessageCenter(И он например создается один на webrequest).

А как обстоят дела с наследованием и несколькими реализациями интерфейса? Вы же эти правила прописываете в кофигурации контейнера? А значит вам придется кропотливо следить за тем в каком случае кто и как решил отрезолвить интерфейс (в одном случае нам тот же кэш нужен, а в другом нет или другой кэш). Конфигурация видимо инициализируется на запуске приложения (привет многопоточность), либо на запуске каждого инстанса (привет производительность).

Да, все это настраивается в композиции. В ОДНОМ месте. А не разбросаны по всему коду. Что насчет производительности, то это почти никогда не вызывает проблем, а если вам выпал шанс один на миллиард, то с этим можно справится(Я не могу придумать ситуации где это будет сделать трудно)
Инвалидацией занимается тотже декоратор.

Т.е. ваш декоратор знаком со всеми сценариями использования данных для каждого сервиса, для которого он сконфигурирован? Вот это уже анти-паттерн God-object.


Дурной запах в том, что при добавлении кеша сервис начинает выполнять две задачи: свою работу и заботу о
кеше.

С чего бы вдруг? BufferedStream выполняет одну задачу — занимается управлением кэшем. Сервис видит все как Stream и вообще не парится. По-моему очень хороший баланс между контролем и расширяемостью.


Если мы используем ДИ, то мы просто добавляем в конструктор параметр IMessageCenter.

Если мы используем DI или DI-контейнер? Одно-дизайн, а другое реализация. Встречный вопрос вам: "А если IMessageCenter имеет несколько реализаций для разных сценариев — будете делать как в статье?"


А если мы используем new, то надо искать везде этот new и добавлять туда новый параметр

Не надо. У вас есть перегрузка конструкторов, фабрики итд.


Если все, что вам нужно — это добавить одну дефолтную реализацию IMessageCenter, то перегрузите конструктор с доп методом (для unit testing), а в старый добавте дефолтный параметр. DI у вас остается, а контейнер уже не так уж и нужен, впрочем тоже возможен.


Вообще ваш пример — один из самых неудачных. Проблема в том, что Message, в обычном состоянии, — это короткоживущий объект. Тот, который должен всегда находиться в поколении молодежи для Garbage Collector. С ним в нормальной ситуации GC легко справится — потратит время только на обход, но то, что вы сделаете с DI контейнером или ServiceLocator — вы его сделаете долгоживущим — а значит вы не только заплатите доп. цену на создании объекта, но еще и на сборке и на поддержании в живом состоянии (дефрагментацию).


Опять же. Если вы колбасите какой-нибудь MVС, чтоб на 10 пользователей и чтобы крутилось на 32 ядрах и 512Gb оперативки — вам может быть и без разницы, но опять же сложность вашего кода тогда не на столько высока чтобы оправдать отдельную конфигурацию для сборки объектов и активно внедряя DI вы скорее всего усложняете проект, а значит зря тратите деньги заказчика, а еще важнее его время (time to market) — клепайте, чтоб работало.


Да, все это настраивается в композиции. В ОДНОМ месте.

Вам нужно все в одном месте… в смысле God-object — это ваша цель? Если так — то это ваш и вашего тех. лида или архитектора выбор. Я бы в наказание за такое дело ставил разработчиков сопровождать этот код в течении нескольких лет без права перехода на другую работу.


Может быть вам нужен один файл и вы почему-то не можете объявить свои классы partial и вытащить конструкторы в этот отдельный файл? Думаете внедрять глобальный статический словарь, ломать стандартное поведение GC, огрести проблемы с отладкой и многопоточностью того стоит? Ну опять же ваш выбор.


Что насчет производительности, то это почти никогда не вызывает проблем

Если у вас нет проблем с производительностью — зачем вы занялись кэшированием? Это было решением продиктованым статистикой и сценариями доступа к данным или "закэширую чтоб два раза не ходить"?

Мы явно не понимаем друг друга ;(. Ни о каких God-object'ах не идет речи. Я не готов писать развернутый ответ, так как для этого потребуется написать книгу. Например https://habrahabr.ru/company/piter/blog/192348/
Ни о каких God-object'ах не идет речи.

Либо вам не доводилось видеть проектов после пары комманд, где кто-то активно любил DI контейнеры, либо вы игнорировали очевидное.


Спасибо за ссылку. Предпочитаю читать в оригинале и от отцов-основателей. Кого-нибудь вроде Фаулера.


Надеюсь мы с вами побеседуем как-нибудь в следующий раз.

Аналог [Dependency(«local»)] для Autofac может кто подсказать?
Посмотрите здесь
http://docs.autofac.org/en/latest/advanced/keyed-services.html

Аттрибут [WithKey(«local»)]
Зарегистрируйтесь на Хабре, чтобы оставить комментарий