Autofac: Скрытие нескольких контравариантных реализаций за одним составным

Я был вызван этим вопросом SO о (.NET 4.0) ковариационной и контравариантной поддержке Autofac, и теперь я пытаюсь добиться чего-то подобного, но без при удаче.

То, что я пытаюсь достичь, - это настроить Autofac таким образом, что когда я разрешаю один конкретный IEventHandler<TEvent> (для демонстрации с использованием container.Resolve, но обычно, конечно, с использованием встраивания конструктора) Autofac вернет мне MultipleDispatchEventHandler<TEvent>, который обертывает все зарегистрированные обработчики событий, которые можно назначить из запрошенного обработчика.

Другими словами, когда я пишу это:

var handler = container
    .GetInstance<IEventHandler<CustomerMovedEvent>>();

handler.Handle(new CustomerMovedEvent());

Что касается дизайна приложения (приведенного ниже), я бы ожидал возвращения MultipleDispatchEventHandler<CustomerMovedEvent>, которое обертывает как CustomerMovedEventHandler, так и NotifyStaffWhenCustomerMovedEventHandler.

Вот дизайн приложения:

// Events:
public class CustomerMovedEvent { }

public class CustomerMovedAbroadEvent : CustomerMovedEvent { }

public class SpecialCustomerMovedEvent : CustomerMovedEvent { }


// Event handler definition (note the 'in' keyword):
public interface IEventHandler<in TEvent> 
{
    void Handle(TEvent e);
}

// Event handler implementations:
public class CustomerMovedEventHandler
    : IEventHandler<CustomerMovedEvent>
{
    public void Handle(CustomerMovedEvent e) { ... }
}

public class NotifyStaffWhenCustomerMovedEventHandler
    : IEventHandler<CustomerMovedEvent>
{
    public void Handle(CustomerMovedEvent e) { ... }
}

public class CustomerMovedAbroadEventHandler
    : IEventHandler<CustomerMovedAbroadEvent>
{
    public void Handle(CustomerMovedAbroadEvent e) { ... }
}

Это определение MultipleDispatchEventHandler<TEvent>, определенное в корне композиции:

// A composite wrapping possibly multiple handlers.
public class MultipleDispatchEventHandler<TEvent>
    : IEventHandler<TEvent>
{
    private IEnumerable<IEventHandler<TEvent>> handlers;

    public MultipleDispatchEventHandler(
        IEnumerable<IEventHandler<TEvent>> handlers)
    {
        this.handlers = handlers;
    }

    public void Handle(TEvent e)
    {
        this.handlers.ToList().ForEach(h => h.Handle(e));
    }
}

Это моя текущая конфигурация:

var builder = new ContainerBuilder();

// Note the use of the ContravariantRegistrationSource (which is 
// available in the latest release of Autofac).
builder.RegisterSource(new ContravariantRegistrationSource());

builder.RegisterAssemblyTypes(typeof(IEventHandler<>).Assembly) 
    .AsClosedTypesOf(typeof(IEventHandler<>));

// UPDATE: I'm registering this last as Kramer suggests.
builder.RegisterGeneric(typeof(MultipleDispatchEventHandler<>))
    .As(typeof(IEventHandler<>)).SingleInstance();

var container = builder.Build();

При текущей конфигурации приложение не работает во время вызова Resolve, со следующим исключением:

Autofac.Core.DependencyResolutionException: круговой компонент обнаружена зависимость: MultipleDispatchEventHandler'1 [[SpecialCustomerMovedEvent]] → IEventHandler'1 [[SpecialCustomerMovedEvent]] [] → MultipleDispatchEventHandler'1 [[SpecialCustomerMovedEvent]].

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

Ответы

Ответ 1

+1 для IEventRaiser<T> по умолчанию @default.kramer. Только для записи, поскольку связанный ответ не предоставляет никакого кода, а конфигурация для этого сценария немного меньше интуитивной из-за общих типов:

builder.RegisterSource(new ContravariantRegistrationSource());

builder.RegisterAssemblyTypes(...)
    .As(t => t.GetInterfaces()
        .Where(i => i.IsClosedTypeOf(typeof(IEventHandler<>)))
        .Select(i => new KeyedService("handler", i)));

builder.RegisterGeneric(typeof(MultipleDispatchEventHandler<>))
    .As(typeof(IEventHandler<>))
    .WithParameter(
         (pi, c) => pi.Name == "handlers",
         (pi, c) => c.ResolveService(
             new KeyedService("handler", pi.ParameterType)));

Ответ 2

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

Рабочий код

Я добавил static int handleCount каждому из обработчиков событий для целей тестирования, например:

public class CustomerMovedEventHandler
    : IEventHandler<CustomerMovedEvent>
{
    public static int handleCount = 0;
    public void Handle(CustomerMovedEvent e) { handleCount++; }
}

Здесь проходит тест, демонстрирующий, что события идут туда, где они должны:

var builder = new ContainerBuilder();

builder.RegisterSource(new Autofac.Features
    .Variance.ContravariantRegistrationSource());

builder.RegisterAssemblyTypes(typeof(IEventHandler<>).Assembly)
    .AsClosedTypesOf(typeof(IEventHandler<>));

builder.RegisterGeneric(typeof(EventRaiser<>))
    .As(typeof(IEventRaiser<>));

var container = builder.Build();

Assert.AreEqual(0, CustomerMovedEventHandler.handleCount);
Assert.AreEqual(0, NotifyStaffWhenCustomerMovedEventHandler.handleCount);
Assert.AreEqual(0, CustomerMovedAbroadEventHandler.handleCount);

container.Resolve<IEventRaiser<CustomerMovedEvent>>()
    .Raise(new CustomerMovedEvent());

Assert.AreEqual(1, CustomerMovedEventHandler.handleCount);
Assert.AreEqual(1, NotifyStaffWhenCustomerMovedEventHandler.handleCount);
Assert.AreEqual(0, CustomerMovedAbroadEventHandler.handleCount);

container.Resolve<IEventRaiser<CustomerMovedAbroadEvent>>()
    .Raise(new CustomerMovedAbroadEvent());

Assert.AreEqual(2, CustomerMovedEventHandler.handleCount);
Assert.AreEqual(2, NotifyStaffWhenCustomerMovedEventHandler.handleCount);
Assert.AreEqual(1, CustomerMovedAbroadEventHandler.handleCount);

container.Resolve<IEventRaiser<SpecialCustomerMovedEvent>>()
    .Raise(new SpecialCustomerMovedEvent());

Assert.AreEqual(3, CustomerMovedEventHandler.handleCount);
Assert.AreEqual(3, NotifyStaffWhenCustomerMovedEventHandler.handleCount);
Assert.AreEqual(1, CustomerMovedAbroadEventHandler.handleCount);

Вы можете видеть, что я использую IEventRaiser<TEvent> вместо составного IEventHandler<TEvent>. Вот как это выглядит:

public interface IEventRaiser<TEvent>
{
    void Raise(TEvent e);
}

public class EventRaiser<TEvent> : IEventRaiser<TEvent>
{
    List<IEventHandler<TEvent>> handlers;

    public EventRaiser(IEnumerable<IEventHandler<TEvent>> handlers)
    {
        this.handlers = handlers.ToList();
    }

    public void Raise(TEvent e)
    {
        handlers.ForEach(h => h.Handle(e));
    }
}

Мысли о дизайне

Избегание составной IEventHandler уверенности делает нашу работу в корне композиции проще. Нам не нужно беспокоиться о рекурсивной композиции или убедиться, что композит является реализацией по умолчанию. Но мы добавили новый интерфейс IEventRaiser, который может выглядеть излишним. Это? Думаю, что нет.

Поднятие события и обработка события - это две разные вещи. IEventHandler - это интерфейс, который связан с обработкой событий. IEventRaiser - это интерфейс, связанный с повышением событий.

Представьте, что я часть кода, которая хочет поднять событие. Если я спрошу IoC для одного IEventHandler, я вводил связь, которая мне не нужна. Мне не нужно знать об этом интерфейсе IEventHandler. Я не должен просить кого-либо о Handle моем событии. Все, что я хочу сделать, это Raise it. Обработка может или не может произойти с другой стороны; это не имеет значения для меня. Я эгоистичен - Я хочу, чтобы интерфейс был создан исключительно для меня и моей потребности в создании событий.

Как участник мероприятия, я собираюсь поднять событие. В качестве обработчика событий я намерен обрабатывать событие. У нас есть два разных намерения, поэтому мы должны иметь два разных интерфейса. Просто потому, что мы можем использовать один и тот же интерфейс, а композит не означает, что мы должны.

Принцип разделения сегментов по-видимому, больше связан с разделением жирных интерфейсов на более тонкие (см. также Интерфейс ролей). В нашем случае у нас нет толстого интерфейса, но я думаю, что мы делаем что-то подобное - "Сегрегирование интерфейса по намерению" .

Еще одна вещь

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

"Интерфейс типа C" - часто потребляется, редко реализуется. Интерфейс "службы". Например, IEventRaiser или ICustomerRepository. Эти интерфейсы, вероятно, имеют только одну реализацию (возможно, немного декорированы), но они потребляются повсеместно по коду, который хочет поднять события или сохранять клиентов.

"Интерфейс типа I" - часто реализуется, редко потребляется. Интерфейс "плагин". Например, IEventHandler<TEvent>. Потребляется только в одном месте (EventRaiser), но реализовано многими классами.

Тот же интерфейс не должен быть как Type C, так и Type I. Это еще одна причина для разделения IEventRaiser (Тип C) на IEventHandler (Тип I).

Я думаю, что составной шаблон применим только к интерфейсам типа С.

Пожалуйста, отредактируйте или прокомментируйте, есть ли стандартная терминология для того, что я назвал интерфейсами типа C и "Тип I.".