Внедрение зависимостей и расположение службы
В настоящее время я оцениваю преимущества и недостатки между DI и SL. Тем не менее, я обнаружил себя в следующем уловке 22, который подразумевает, что я должен просто использовать SL для всего и только вставлять контейнер IoC в каждый класс.
DI Catch 22:
Некоторые зависимости, такие как Log4Net, просто не подходят для DI. Я называю эти мета-зависимости и чувствую, что они должны быть непрозрачными для вызова кода. Мое оправдание заключается в том, что если простой класс "D" был первоначально реализован без ведения журнала, а затем растет, требуя ведения журнала, то зависимые классы "A", "B" и "C" теперь должны каким-то образом получить эту зависимость и передать ее из "A" - "D" (при условии, что "A" составляет "B", "B" - "C" и т. Д.). Теперь мы внесли существенные изменения в код только потому, что нам требуется вход в один класс.
Поэтому нам необходим непрозрачный механизм для получения мета-зависимостей. На ум приходят два: Singleton и SL. Первый имеет известные ограничения, в первую очередь в отношении жестких возможностей видимости: в лучшем случае синглтон будет использовать абстрактную фабрику, которая хранится в области приложения (т. Е. В статической переменной). Это допускает некоторую гибкость, но не идеально.
Лучшим решением было бы внедрить контейнер IoC в такие классы, а затем использовать SL из этого класса для разрешения этих мета-зависимостей из контейнера.
Отсюда уловка 22: поскольку класс теперь внедряется с контейнером IoC, то почему бы не использовать его для разрешения всех других зависимостей?
Буду очень признателен за ваши мысли:)
10 ответов
Поскольку класс теперь внедряется в контейнер IoC, то почему бы не использовать его для разрешения всех других зависимостей?
Использование шаблона локатора службы полностью устраняет одну из основных точек внедрения зависимости. Смысл внедрения зависимости заключается в том, чтобы сделать зависимости явными. Как только вы скрываете эти зависимости, не делая их явными параметрами в конструкторе, вы больше не делаете полноценное внедрение зависимостей.
Это все конструкторы для класса с именем Foo
(установить на тему песни Джонни Кэша):
Неправильно:
public Foo() {
this.bar = new Bar();
}
Неправильно:
public Foo() {
this.bar = ServiceLocator.Resolve<Bar>();
}
Неправильно:
public Foo(ServiceLocator locator) {
this.bar = locator.Resolve<Bar>();
}
Правильно:
public Foo(Bar bar) {
this.bar = bar;
}
Только последний делает зависимость от Bar
явный.
Что касается ведения журнала, есть правильный способ сделать это, не проникая в код вашего домена (это не должно происходить, но если это так, то вы используете период внедрения зависимости). Удивительно, но контейнеры IoC могут помочь с этой проблемой. Начните здесь.
Сервисный локатор является антишаблоном, по причинам, превосходно описанным по адресу http://blog.ploeh.dk/2010/02/03/ServiceLocatorIsAnAntiPattern.aspx. С точки зрения ведения журнала, вы можете рассматривать это как зависимость, как и любую другую, и внедрять абстракцию с помощью конструктора или внедрения свойства.
Единственная разница с log4net заключается в том, что ему требуется тип вызывающего абонента, который использует сервис. Использование Ninject (или другого контейнера) Как я могу узнать тип, который запрашивает службу? описывает, как вы можете решить эту проблему (он использует Ninject, но применим к любому контейнеру IoC).
В качестве альтернативы вы можете подумать о ведении журнала как о сквозной проблеме, которая не подходит для вашего кода бизнес-логики, и в этом случае вы можете использовать перехват, который обеспечивается многими контейнерами IoC. http://msdn.microsoft.com/en-us/library/ff647107.aspx описывает использование перехвата с Unity.
Мое мнение таково, что это зависит. Иногда одно лучше, а иногда другое. Но я бы сказал, что в целом я предпочитаю DI. Есть несколько причин для этого.
Когда зависимость вводится каким-либо образом в компонент, она может рассматриваться как часть его интерфейса. Таким образом, пользователю компонента проще предоставлять эти зависимости, потому что они видны. В случае внедренного SL или Static SL эти зависимости скрыты и использование компонента немного сложнее.
Внедренные зависимости лучше подходят для модульного тестирования, потому что вы можете просто их высмеять. В случае SL вы должны снова установить зависимости Locator + mock. Так что больше работы.
Иногда ведение журнала может быть реализовано с использованием AOP, чтобы оно не сочеталось с бизнес-логикой.
В противном случае возможны следующие варианты:
- используйте необязательную зависимость (например, свойство setter), и для модульного теста вы не вводите логгер. Контейнер IOC позаботится об их автоматической настройке, если вы будете работать в производстве.
- Когда у вас есть зависимость, которую использует почти каждый объект вашего приложения (наиболее распространенным примером является объект "logger"), это один из немногих случаев, когда одноэлементный анти-шаблон становится хорошей практикой. Некоторые люди называют эти "хорошие одиночные игры" окружающим контекстом: http://aabs.wordpress.com/2007/12/31/the-ambient-context-design-pattern-in-net/
Конечно, этот контекст должен быть настраиваемым, чтобы вы могли использовать заглушку / макет для модульного тестирования. Другое предлагаемое использование AmbientContext, это поместить текущего провайдера даты / времени туда, чтобы вы могли заглушить его во время модульного тестирования, и ускорить время, если хотите.
Это касается "Сервисного локатора - это анти-паттерн" Марка Симана. Я могу ошибаться здесь. Но я просто подумал, что тоже должен поделиться своими мыслями.
public class OrderProcessor : IOrderProcessor
{
public void Process(Order order)
{
var validator = Locator.Resolve<IOrderValidator>();
if (validator.Validate(order))
{
var shipper = Locator.Resolve<IOrderShipper>();
shipper.Ship(order);
}
}
}
Метод Process() для OrderProcessor фактически не следует принципу "инверсии управления". Это также нарушает принцип единой ответственности на уровне методов. Почему метод должен быть связан с созданием
объекты (через новый или любой класс SL), что нужно для достижения чего-либо.
Вместо того, чтобы метод Process() создавал объекты, конструктор может фактически иметь параметры для соответствующих объектов (читать зависимости), как показано ниже. Тогда КАК сервисный локатор может отличаться от МОК?
контейнер. И это также поможет в модульном тестировании.
public class OrderProcessor : IOrderProcessor
{
public OrderProcessor(IOrderValidator validator, IOrderShipper shipper)
{
this.validator = validator;
this.shipper = shipper;
}
public void Process(Order order)
{
if (this.validator.Validate(order))
{
shipper.Ship(order);
}
}
}
//Caller
public static void main() //this can be a unit test code too.
{
var validator = Locator.Resolve<IOrderValidator>(); // similar to a IOC container
var shipper = Locator.Resolve<IOrderShipper>();
var orderProcessor = new OrderProcessor(validator, shipper);
orderProcessor.Process(order);
}
Я использовал инфраструктуру Google Guice DI в Java и обнаружил, что она делает гораздо больше, чем просто облегчает тестирование. Например, мне требовался отдельный журнал для каждого приложения (не для класса) с дополнительным требованием, чтобы весь мой общий код библиотеки использовал регистратор в текущем контексте вызова. Внедрение регистратора сделало это возможным. По общему признанию, весь код библиотеки должен был быть изменен: регистратор был введен в конструкторы. Сначала я сопротивлялся этому подходу из-за всех необходимых изменений кодирования; в конце концов я понял, что изменения имели много преимуществ:
- Код стал проще
- Код стал намного надежнее
- Зависимости класса стали очевидными
- Если было много зависимостей, это было явным признаком того, что классу необходимо рефакторинг
- Статические синглтоны были устранены
- Потребность в объектах сеанса или контекста исчезла
- Многопоточность стала намного проще, потому что контейнер DI мог быть построен так, чтобы содержать только один поток, таким образом устраняя случайное перекрестное загрязнение
Само собой разумеется, я теперь большой поклонник DI, и использую его для всех, кроме самых тривиальных приложений.
Я знаю, что этот вопрос немного староват, я просто подумал, что могу дать свой вклад.
На самом деле, в 9 случаях из 10 вы действительно не нуждаетесь в SL и должны полагаться на DI. Однако в некоторых случаях вы должны использовать SL. Одна из областей, в которой я использую SL (или ее разновидность) - это разработка игр.
Еще одним преимуществом SL (на мой взгляд) является возможность обойти internal
классы.
Ниже приведен пример:
internal sealed class SomeClass : ISomeClass
{
internal SomeClass()
{
// Add the service to the locator
ServiceLocator.Instance.AddService<ISomeClass>(this);
}
// Maybe remove of service within finalizer or dispose method if needed.
internal void SomeMethod()
{
Console.WriteLine("The user of my library doesn't know I'm doing this, let's keep it a secret");
}
}
public sealed class SomeOtherClass
{
private ISomeClass someClass;
public SomeOtherClass()
{
// Get the service and call a method
someClass = ServiceLocator.Instance.GetService<ISomeClass>();
someClass.SomeMethod();
}
}
Как вы можете видеть, пользователь библиотеки не имеет ни малейшего представления, что этот метод был вызван, потому что мы не делали DI, а не то, что мы могли бы это сделать в любом случае.
Мы пришли к компромиссу: используйте DI, но объедините зависимости верхнего уровня в объект, избегая рефакторинга ада, если эти зависимости изменятся.
В приведенном ниже примере мы можем добавить "ServiceDependencies" без необходимости рефакторинга всех производных зависимостей.
Пример:
public ServiceDependencies{
public ILogger Logger{get; private set;}
public ServiceDependencies(ILogger logger){
this.Logger = logger;
}
}
public abstract class BaseService{
public ILogger Logger{get; private set;}
public BaseService(ServiceDependencies dependencies){
this.Logger = dependencies.Logger; //don't expose 'dependencies'
}
}
public class DerivedService(ServiceDependencies dependencies,
ISomeOtherDependencyOnlyUsedByThisService additionalDependency)
: base(dependencies){
//set local dependencies here.
}
Я знаю, что люди действительно говорят, что DI - это единственная хорошая модель МОК, но я не понимаю этого. Я постараюсь немного продать SL. Я буду использовать новую платформу MVC Core, чтобы показать вам, что я имею в виду. Первые двигатели DI действительно сложны. Что на самом деле имеют в виду, когда говорят DI, так это использование какой-то среды, такой как Unity, Ninject, Autofac..., которая делает всю тяжелую работу за вас, где SL может быть таким же простым, как создание фабричного класса. Для небольших быстрых проектов это простой способ сделать IOC, не изучая целую структуру для правильного DI, они могут быть не такими сложными для изучения, но все же. Теперь к проблеме, которой может стать DI. Я буду использовать цитату из документов MVC Core.
"ASP.NET Core спроектирован с нуля, чтобы поддерживать и использовать внедрение зависимостей". Большинство людей говорят, что о DI "99% вашей кодовой базы не должны знать о вашем контейнере IoC". Так зачем им проектировать с нуля, если об этом должен знать только 1% кода, разве старый MVC не поддерживает DI? Ну, это большая проблема DI, это зависит от DI. Чтобы все работало "КАК ЭТО ДОЛЖНО БЫТЬ СДЕЛАНО", нужно много работать. Если вы посмотрите на новый Action Injection, это не зависит от DI, если вы используете [FromServices]
приписывать. Теперь DI люди скажут НЕТ, вы должны пойти с Фабриками, а не с этим, но, как вы можете видеть, даже люди, делающие MVC, сделали это правильно. Проблема DI видна в фильтрах, а также посмотрите, что нужно сделать, чтобы получить DI в фильтре.
public class SampleActionFilterAttribute : TypeFilterAttribute
{
public SampleActionFilterAttribute():base(typeof(SampleActionFilterImpl))
{
}
private class SampleActionFilterImpl : IActionFilter
{
private readonly ILogger _logger;
public SampleActionFilterImpl(ILoggerFactory loggerFactory)
{
_logger = loggerFactory.CreateLogger<SampleActionFilterAttribute>();
}
public void OnActionExecuting(ActionExecutingContext context)
{
_logger.LogInformation("Business action starting...");
// perform some business logic work
}
public void OnActionExecuted(ActionExecutedContext context)
{
// perform some business logic work
_logger.LogInformation("Business action completed.");
}
}
}
Где, если бы вы использовали SL, вы могли бы сделать это с помощью var _logger = Locator.Get();. И тогда мы подходим к представлениям. При всей доброй воле относительно DI они должны были использовать SL для представлений. новый синтаксис @inject StatisticsService StatsService
такой же как var StatsService = Locator.Get<StatisticsService>();
, Наиболее рекламируемой частью DI является модульное тестирование. Но то, что делают люди, - это просто тестирование там фиктивных сервисов без цели или необходимость подключать туда движок DI для проведения реальных тестов. И я знаю, что вы можете делать что угодно плохо, но люди в конечном итоге делают локатор SL, даже если они не знают, что это такое. Где не так много людей делают DI, даже не читая сначала. Моя самая большая проблема с DI состоит в том, что пользователь класса должен знать о внутренней работе класса в другом, чтобы использовать его.
SL может быть хорошо использован и имеет ряд преимуществ, в первую очередь, его простота.
Если в примере в качестве зависимости используется только log4net, то вам нужно только сделать это:
ILog log = LogManager.GetLogger(typeof(Foo));
Нет смысла вводить зависимость, так как log4net обеспечивает детальное ведение журнала, принимая тип (или строку) в качестве параметра.
Кроме того, DI не коррелирует с SL. ИМХО цель ServiceLocator для разрешения необязательных зависимостей.
Например: если SL предоставляет интерфейс ILog, я напишу логирование daa.
Для DI вам нужна жесткая ссылка на сборку внедренного типа? Я не вижу, чтобы кто-нибудь об этом говорил. Для SL я могу указать своему преобразователю, где динамически загружать мой тип, когда это необходимо, из config.json или аналогичного. Кроме того, если ваша сборка содержит несколько тысяч типов и их наследование, нужны ли вам тысячи каскадных вызовов поставщику службы для их регистрации? Вот о чем я вижу много разговоров. Большинство из них говорят о преимуществах DI и о том, что это вообще такое. Когда дело доходит до того, как реализовать его в.net, они представили метод расширения для добавления ссылки на сборку жестко связанных типов. Для меня это не очень разобщает.