Pull to refresh
16
0
Ivan Zaruba @ivanzaruba

Developer

Send message
Здесь пример того, как в SCD можно вынести в подпапку lib все, что можно. На что следует обратить внимание:
— путь к подпапке указан в runtimeconfig
— поскольку в additional probing paths поиск зависимостей происходит по относительному пути (path пакета из секции libraries + относительный путь из секции dependencies), то deps-файл пришлось «подправить», убрав лишние части.
— hostpolicy.dll должна быть в т.н. package layout, т.е. ее нельзя положить в корень lib
— пути coreclr.dll и corejit.dll должны начинаться с символа '/'
UPD additional-deps работает только для Portable приложений, для Standalone deps-файл всегда один и также должен находиться в корне
В .NET Core 2.0 также можно использовать событие AppDomain.AssemblyResolve.
Однако оно вызывается уже после запуска Core CLR, если не удалось найти сборку в probe paths.
Суть использования deps-файла — проверить до запуска CLR, что все управляемые зависимости на месте и передать их в виде списка (TPA), чтобы AppDomain тратил меньше времени на поиск.
Для Standalone полностью убрать все файлы из корня не получится:
— библиотека hostfxr.dll должна находится строго в \host\fxr\[x.y.z]\ (FDD) или папке с приложением (SCD);
— для SCD файл приложения MyApp.dll и deps-файл должны быть в одной папке с запускаемым файлом MyApp.exe. И тут интересный момент, сами файлы могут находится в другом месте и быть переопределены аргументами additional-deps и additionalprobingpath, но в корне могут быть пустые файлы с такими же названиями, поскольку наличие/отсутствие этих файлов по соглашению определяют тип выполнения
Довольно смело утверждать, что Singleton нарушает SRP. Холиварный вопрос)) Singleton объект ограничивает кол-во созданных экземпляров себя, а это можно рассматривать, как часть бизнес-логики объекта. Следовательно, нарушения SRP нет.

Например,
когда не может логически существовать более одного экземпляра объекта. Например NSApplication.
Абсолютно согласен, плюсую :)
Вообще существует мнение, что ServiceLocator — это антипаттерн.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Конечно, абстракцию можно задать и классом, иногда даже не абстрактным (тот же 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»), потому что этого функционала нет в «коробке».
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. И каждый тип будет в свою очередь иметь свои зависимости. Если везде использовать конкретные типы вместо интерфейсов, может полдня уйти только на изменения типов.
Посмотрите здесь
http://docs.autofac.org/en/latest/advanced/keyed-services.html

Аттрибут [WithKey(«local»)]
Символы определяют, какой код будет скомпилирован, а Environment, какой будет выполнен в зависимости от значения переменной окружения.
Преимущество Environment над conditional compilation symbols, например, в том, что Envitonment определяет переменную для всего решения, в то время как символы действуют только в рамках одной сборки. Но это что касается их общего функционала, т.е. то, в чем их можно сравнить, а вообще имхо у этих механизмов разное предназначение.
С удовольствием подскажу :)

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

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

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

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

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

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

Надеюсь, я правильно понял Ваши вопросы. Если что-нибудь остается непонятным, спрашивайте — с удовольствием отвечу :)
инъекция

Внедрение.

«Инъекция» более используемый термин, поэтому написал по привычке :) Спасибо за поправку!

Не «Environment это глобальная переменная»

Согласен с уточнением. Я в данной статье хотел дать абстрактное понятие Environment, как некая абстрактная глобальная переменная, а для более конкретного определения дается ссылка на документацию. Спасибо большое!)

Information

Rating
Does not participate
Location
Харьков, Харьковская обл., Украина
Date of birth
Registered
Activity