【问题标题】:Autofac: Hiding multiple contravariant implementations behind one compositeAutofac:在一个复合材料后面隐藏多个逆变实现
【发布时间】:2011-08-21 15:04:08
【问题描述】:

this SO question 触发了我对 Autofac 的 (.NET 4.0) 协变和逆变支持,现在我正在尝试实现类似的东西,但没有任何运气。

我想要实现的是以这样的方式配置 Autofac,即当我解析单个具体的 IEventHandler<TEvent>(为了使用 container.Resolve 进行演示,但通常当然使用构造函数注入)时,Autofac 将返回给我一个MultipleDispatchEventHandler<TEvent> 包装了所有可从请求的处理程序分配的已注册事件处理程序。

换句话说,当我写这个时:

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

handler.Handle(new CustomerMovedEvent());

关于应用程序设计(如下所示),我希望返回一个包含CustomerMovedEventHandlerNotifyStaffWhenCustomerMovedEventHandlerMultipleDispatchEventHandler&lt;CustomerMovedEvent&gt;

这是应用程序设计:

// 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&lt;TEvent&gt;的定义,在Composition Root中定义:

// 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]]。

现在的问题当然是:如何修复配置(或设计)以支持此功能?

【问题讨论】:

  • 我们是否需要使用 autofac 来解决这个问题?或者您只是想弄清楚各种 IoC 容器是如何解决这种特殊情况的?
  • @thekip:你可能已经看过我的问题历史了 :-)。虽然我很想知道如何使用 Castle Windsor、StructureMap 和 Unity 等容器来做到这一点,但我目前对 Autofac 感兴趣,因为该容器最近添加了对逆变的显式支持。我以后可能会针对这些容器添加问题,所以请不要为其他容器回答。当然,我知道如何使用 Simple Injector 来做到这一点;-)
  • 当然,如果您将IEventPublisher&lt;TEvent&gt;IEventHandler&lt;TEvent&gt; 分开,您的示例场景很容易解决。无论如何,这可能是一个好主意,这取决于您问谁 - 为事件发起者和事件消费者提供单独的界面可以降低耦合度。但是没有复合材料,我假设您对复合材料比四处走动的客户更感兴趣:)
  • @default:你能更新/附加你的答案来告诉我这个吗?我很感兴趣。

标签: c# dependency-injection ioc-container autofac covariance


【解决方案1】:

我将把它作为一个单独的答案,而不是修改我的另一个答案。这个解决了示例场景而不使用复合。

工作代码

我为每个事件处理程序添加了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&lt;TEvent&gt;,而不是复合IEventHandler&lt;TEvent&gt;。下面是它的外观:

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它。处理可能会或可能不会发生在另一边;这与我无关。我很自私 - 我想要一个专门为我和我需要引发事件的界面创建。

作为一个事件发起者,我打算发起一个事件。作为事件处理程序,我打算处理一个事件。我们有两个不同的意图,所以我们应该有两个不同的接口。仅仅因为我们可以使用相同的接口和组合并不意味着我们应该这样做。

Interface Segregation Principle 似乎更倾向于将胖接口拆分成更薄的接口(另请参阅Role Interface)。在我们的例子中,我们没有胖接口,但我认为我们正在做类似的事情——“Interface Segregation by Intent”。

还有一件事

在写这个答案时,我几乎表达了一个我认为我们很多人都熟悉的设计成语,但我认为我们没有标准的术语。

“Type C 接口” - 经常使用,很少实现。一个“服务”接口。例如,IEventRaiserICustomerRepository。这些接口可能只有一个实现(可能稍作修饰),但它们被想要引发事件或拯救客户的代码到处使用。

“类型 I 接口” - 经常实现,很少使用。一个“插件”界面。例如,IEventHandler&lt;TEvent&gt;。仅在一个地方使用(EventRaiser),但由许多类实现。

同一个接口不应同时是 C 型和 I 型。这是将 IEventRaiser(C 型)与 IEventHandler(I 型)分开的另一个原因。

我认为复合模式只适用于 Type C 接口。

如果有我称之为“C 型”和“I 型”接口的标准术语,请编辑或评论。

【讨论】:

  • 不是我希望的答案,但还是不错的答案 ;-)
【解决方案2】:

@default.kramer 为IEventRaiser&lt;T&gt; +1。仅作记录,因为链接的答案没有提供任何代码,并且由于涉及的泛型类型,此场景的配置有点不直观:

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)));

【讨论】:

  • 您注册 KeyedService 的方式太棒了!我需要重新审视其中的一些As 重载。我对此进行了测试,在进行了一些明显的修改后,它按预期工作。
  • 酷 - 感谢您查看。我们需要一个在 JavaScript 中为 Stack Overflow 实现的 C# 语法检查器:)
  • @Nicholas:(c, pi) =&gt; pi.Name == "handlers" 行无法编译,因为 pi 参数的类型为 IComponentContext 并且不包含 Name 属性。我不确定如何解决这个问题。
  • @Steven 显然我的编辑没有通过 - 只需将其更改为 (pi, c)。此外,最后一个 KeyedService 构造函数的参数是相反的。
  • @Nicholas:这太狡猾了。 ContravariantRegistrationSource 必须其他注册之前注册。否则一切都会失败。我将此行添加到您的答案中以明确这一点。
猜你喜欢
  • 2013-01-07
  • 1970-01-01
  • 2018-12-19
  • 1970-01-01
  • 1970-01-01
  • 2019-05-03
  • 2017-07-09
  • 2019-05-07
  • 1970-01-01
相关资源
最近更新 更多