Использование структур внедрения зависимостей для классов с множеством зависимостей - PullRequest
36 голосов
/ 12 октября 2008

Я изучал различные структуры внедрения зависимостей для .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 ]

25 голосов
/ 16 октября 2009

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

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

Закон Никола о SRP и DI

"Любой класс, имеющий более 3 зависимости должны быть опрошены для Нарушение ПСП "

(Чтобы избежать длинного ответа, я подробно разместил свои ответы на IoC и SRP сообщение в блоге)

8 голосов
/ 14 октября 2008

Первое: Добавьте простые зависимости к вашему конструктору по мере необходимости. Нет необходимости добавлять каждый тип в каждый конструктор, просто добавьте те, которые вам нужны. Нужен еще один, просто разверните конструктор. Производительность не должна быть большой вещью, так как большинство этих типов, вероятно, будут одиночными, поэтому уже созданы после первого вызова. Не используйте статический DI-контейнер для создания других объектов. Вместо этого добавьте DI-контейнер к себе, чтобы он мог разрешить себя как зависимость. Так что-то вроде этого (предполагая Unity на данный момент)

IUnityContainer container = new UnityContainer();
container.RegisterInstance<IUnityContainer>(container);

Таким образом, вы можете просто добавить зависимость от IUnityContainer и использовать ее для создания дорогих или редко необходимых объектов. Основным преимуществом является то, что при модульном тестировании намного проще, так как нет статических зависимостей.

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

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

4 голосов
/ 19 октября 2009

У меня есть похожий случай, связанный с "дорогим созданием, и может * использоваться ", где в моей собственной реализации 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&lt;T&gt;"/> 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&lt;T&gt;"/> 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();
}
4 голосов
/ 13 октября 2008

Первый:

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

Второе:

Пройдите на какой-нибудь стройке или фабрике.

Третье:

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

2 голосов
/ 12 октября 2008

Первый:

Вы можете подойти к нему, создав контейнер для хранения ваших «неинтересных» зависимостей (ILog, ICache, IApplicationSettings и т. Д.), И использовать инжектор конструктора, чтобы внедрить его, затем внутри конструктора, гидрировать поля службы из контейнера .Разрешить() ? Я не уверен, что мне это понравится, но, вполне возможно, это возможно.

В качестве альтернативы вы можете использовать новый общий интерфейс IServiceLocator (http://blogs.msdn.com/gblock/archive/2008/10/02/iservicelocator-a-step-toward-ioc-container-service-locator-detente.aspx) вместо введения зависимостей?

Второе:

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

0 голосов
/ 14 октября 2008

Чтобы частично ответить на мой первый вопрос, я только что нашел сообщение в блоге Джереми Миллера, показывающее, как можно использовать структурную карту и внедрение сеттера для автоматического заполнения открытых свойств. ваших объектов. Он использует 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>();
...