Использование структур внедрения Dependency Injection для классов с множеством зависимостей
Я смотрел на различные структуры внедрения зависимостей для.NET, так как чувствую, что проект, над которым я работаю, очень выиграл бы от этого. Хотя я думаю, что хорошо понимаю возможности этих фреймворков, мне все еще немного неясно, как лучше всего внедрить их в большую систему. Большинство демонстраций (понятно) имеют тенденцию быть довольно простыми классами, которые имеют одну или две зависимости.
У меня три вопроса...
Во-первых, как вы справляетесь с этими общими, но неинтересными зависимостями, например, ILog, IApplicationSettings, IPermissions, IAudit. Кажется излишним для каждого класса иметь их в качестве параметров в своем конструкторе. Было бы лучше использовать статический экземпляр контейнера DI, чтобы получить их, когда они необходимы?
MyClass(ILog log, IAudit audit, IPermissions permissions, IApplicationSettings settings)
// ... versus ...
ILog log = DIContainer.Get<ILog>();
Во-вторых, как вы подходите к зависимостям, которые могут использоваться, но могут быть дорогостоящими в создании. Пример - класс может зависеть от интерфейса ICDBurner, но не желать, чтобы создавалась конкретная реализация, если функция записи компакт-дисков фактически не использовалась. Вы передаете интерфейсы фабрикам (например, ICDBurnerFactory) в конструкторе, или вы снова идете с каким-то статическим способом прямого доступа к DI-контейнеру и запрашиваете его там, где это необходимо?
В-третьих, предположим, что у вас есть большое приложение Windows Forms, в котором компонент GUI верхнего уровня (например, MainForm) является родителем потенциально сотен подпанелей или модальных форм, каждая из которых может иметь несколько зависимостей. Означает ли это, что MainForm должен быть настроен так, чтобы иметь в качестве зависимостей надмножество всех зависимостей его дочерних элементов? И если бы вы это сделали, разве это не привело бы к созданию огромного самонадувающегося монстра, который создает каждый отдельный класс, который может понадобиться ему в момент создания MainForm, тратя впустую время и память в процессе?
6 ответов
Первое: добавьте простые зависимости в ваш конструктор по мере необходимости. Нет необходимости добавлять каждый тип в каждый конструктор, просто добавьте те, которые вам нужны. Нужен еще один, просто разверните конструктор. Производительность не должна быть большой вещью, так как большинство этих типов, вероятно, будут одиночными, поэтому уже созданы после первого вызова. Не используйте статический DI-контейнер для создания других объектов. Вместо этого добавьте DI-контейнер к себе, чтобы он мог разрешить себя как зависимость. Так что-то вроде этого (предполагая Unity на данный момент)
IUnityContainer container = new UnityContainer();
container.RegisterInstance<IUnityContainer>(container);
Таким образом, вы можете просто добавить зависимость от IUnityContainer и использовать ее для создания дорогих или редко необходимых объектов. Основное преимущество заключается в том, что при модульном тестировании намного проще, так как нет статических зависимостей.
Второе: не нужно переходить на заводской класс. Используя описанную выше технику, вы можете использовать сам контейнер DI для создания дорогих объектов, когда это необходимо.
Третье: добавьте контейнер DI и легкие одноэлементные зависимости в основную форму и создайте остальное через контейнер DI по мере необходимости. Занимает немного больше кода, но, как вы сказали, стоимость запуска и потребление памяти основной формой будут расти, если вы создадите все во время запуска.
Что ж, в то время как вы можете сделать это, как описано в других ответах, я считаю, что в отношении вашего примера есть более важная вещь, на которую нужно ответить, а именно то, что вы, вероятно, нарушаете принцип SRP, когда класс имеет много зависимостей.
То, что я хотел бы рассмотреть в вашем примере, это разбить класс на пару более согласованных классов с целенаправленными проблемами, и, таким образом, число их зависимостей уменьшится.
Никольский закон о СРП и ДИ
"Любой класс, имеющий более 3 зависимостей, должен быть допрошен за нарушение SRP"
(Чтобы избежать длинного ответа, я подробно разместил свои ответы в блоге IoC и SRP)
У меня есть похожий случай, связанный с "дорогим созданием и его можно использовать", где в моей собственной реализации IoC я добавляю автоматическую поддержку сервисов фабрики.
В основном вместо этого:
public SomeService(ICDBurner burner)
{
}
Вы бы сделали это:
public SomeService(IServiceFactory<ICDBurner> burnerFactory)
{
}
ICDBurner burner = burnerFactory.Create();
Это имеет два преимущества:
- За кулисами сервисный контейнер, который разрешил вашу службу, также используется для разрешения программы записи, если и когда это требуется
- Это устраняет проблемы, с которыми я сталкивался ранее в подобном случае, когда типичным способом было бы внедрить сам сервисный контейнер в качестве параметра для вашего сервиса, в основном говоря: "Этот сервис требует других сервисов, но я не собираюсь легко скажу какие
Заводской объект довольно прост в изготовлении и решает множество проблем.
Вот мой заводской класс:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using LVK.IoC.Interfaces;
using System.Diagnostics;
namespace LVK.IoC
{
/// <summary>
/// This class is used to implement <see cref="IServiceFactory{T}"/> for all
/// services automatically.
/// </summary>
[DebuggerDisplay("AutoServiceFactory (Type={typeof(T)}, Policy={Policy})")]
internal class AutoServiceFactory<T> : ServiceBase, IServiceFactory<T>
{
#region Private Fields
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private readonly String _Policy;
#endregion
#region Construction & Destruction
/// <summary>
/// Initializes a new instance of the <see cref="AutoServiceFactory<T>"/> class.
/// </summary>
/// <param name="serviceContainer">The service container involved.</param>
/// <param name="policy">The policy to use when resolving the service.</param>
/// <exception cref="ArgumentNullException"><paramref name="serviceContainer"/> is <c>null</c>.</exception>
public AutoServiceFactory(IServiceContainer serviceContainer, String policy)
: base(serviceContainer)
{
_Policy = policy;
}
/// <summary>
/// Initializes a new instance of the <see cref="AutoServiceFactory<T>"/> class.
/// </summary>
/// <param name="serviceContainer">The service container involved.</param>
/// <exception cref="ArgumentNullException"><paramref name="serviceContainer"/> is <c>null</c>.</exception>
public AutoServiceFactory(IServiceContainer serviceContainer)
: this(serviceContainer, null)
{
// Do nothing here
}
#endregion
#region Public Properties
/// <summary>
/// Gets the policy that will be used when the service is resolved.
/// </summary>
public String Policy
{
get
{
return _Policy;
}
}
#endregion
#region IServiceFactory<T> Members
/// <summary>
/// Constructs a new service of the correct type and returns it.
/// </summary>
/// <returns>The created service.</returns>
public IService<T> Create()
{
return MyServiceContainer.Resolve<T>(_Policy);
}
#endregion
}
}
По сути, когда я создаю сервисный контейнер из своего класса конструктора сервисных контейнеров, всем регистрациям сервисов автоматически предоставляется другой сопутствующий сервис, реализующий IServiceFactory для этого сервиса, если только программист явно не зарегистрировался в себе для этого сервиса. Затем используется вышеупомянутая служба с одним параметром, определяющим политику (которая может быть нулевой, если политики не используются).
Это позволяет мне сделать это:
var builder = new ServiceContainerBuilder();
builder.Register<ISomeService>()
.From.ConcreteType<SomeService>();
using (var container = builder.Build())
{
using (var factory = container.Resolve<IServiceFactory<ISomeService>>())
{
using (var service = factory.Instance.Create())
{
service.Instance.DoSomethingAwesomeHere();
}
}
}
Конечно, более типичное использование будет с вашим объектом CD Burner. В приведенном выше коде я бы разрешил службу вместо, конечно, но это иллюстрация того, что происходит.
Так что с вашим сервисом записи компакт-дисков:
var builder = new ServiceContainerBuilder();
builder.Register<ICDBurner>()
.From.ConcreteType<CDBurner>();
builder.Register<ISomeService>()
.From.ConcreteType<SomeService>(); // constructor used in the top of answer
using (var container = builder.Build())
{
using (var service = container.Resolve<ISomeService>())
{
service.Instance.DoSomethingHere();
}
}
внутри службы теперь вы можете иметь службу, фабричную службу, которая знает, как разрешить службу записи компакт-дисков по запросу. Это полезно по следующим причинам:
- Возможно, вы захотите разрешить несколько служб одновременно (записать два диска одновременно?)
- Вам это может не понадобиться, и его создание может быть дорогостоящим, поэтому вы можете разрешить его только при необходимости.
- Возможно, вам придется разрешать, удалять, разрешать, удалять несколько раз, вместо того, чтобы надеяться / пытаться очистить существующий экземпляр службы
- Вы также отмечаете в своем конструкторе, какие сервисы вам нужны, а какие вам могут понадобиться
Вот два одновременно:
using (var service1 = container.Resolve<ISomeService>())
using (var service2 = container.Resolve<ISomeService>())
{
service1.Instance.DoSomethingHere();
service2.Instance.DoSomethingHere();
}
Вот два после друг друга, не повторно используя тот же сервис:
using (var service = container.Resolve<ISomeService>())
{
service.Instance.DoSomethingHere();
}
using (var service = container.Resolve<ISomeService>())
{
service.Instance.DoSomethingElseHere();
}
Первый:
При необходимости вы можете внедрить эти объекты как члены, а не в конструктор. Таким образом, вам не нужно вносить изменения в конструктор по мере изменения вашего использования, и вам также не нужно использовать статический элемент.
Во-вторых:
Пройдите в какой-то строй или завод.
В третьих:
Любой класс должен иметь только те зависимости, которые ему необходимы. Подклассы должны быть введены с их собственными конкретными зависимостями.
Первый:
Вы можете подойти к нему, создав контейнер для хранения ваших "неинтересных" зависимостей (ILog, ICache, IApplicationSettings и т. Д.), И использовать инъекцию в конструктор, чтобы внедрить это, а затем в конструктор, гидрировать поля службы из контейнера.Resolve() Я не уверен, что мне это понравится, но, ну, это возможно.
В качестве альтернативы, вы можете использовать новый общий интерфейс IServiceLocator ( http://blogs.msdn.com/gblock/archive/2008/10/02/iservicelocator-a-step-toward-ioc-container-service-locator-detente.aspx) вместо внедрения зависимостей?
Во-вторых:
Вы могли бы использовать инъекцию сеттера для дополнительных зависимостей / по требованию? Я думаю, что я бы пошел на инъекционные заводы и новые оттуда по требованию.
Чтобы частично ответить на мой первый вопрос, я только что нашел пост в блоге Джереми Миллера, показывающий, как можно использовать Структурную карту и внедрение сеттера для автоматического заполнения общедоступных свойств ваших объектов. Он использует ILogger в качестве примера:
var container = new Container(r =>
{
r.FillAllPropertiesOfType<ILogger>().TheDefault.Is
.ConstructedBy(context => new Logger(context.ParentType));
});
Это означает, что любые классы со свойством ILogger, например:
public class ClassWithLogger
{
public ILogger Logger { get; set; }
}
public class ClassWithLogger2
{
public ILogger Logger { get; set; }
}
их свойство Logger будет автоматически установлено при создании:
container.GetInstance<ClassWithLogger>();