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.".