项目
版本

如何根据上下文选择服务实现?

有时,你可能需要注册多个 组件 ,它们都提供了相同的 服务 ,但希望在不同实例中选择使用哪个组件。让我们以一个简单的订单处理系统为例:

  • 发货处理器:负责将订单内容物理寄送。
  • 通知处理器:当订单状态改变时,向用户发送警报。

在这个系统中,发货处理器可能需要支持不同的“插件”来实现不同的配送方式,如邮政、UPS、FedEx 等。通知处理器可能也需要不同的 “插件” 来支持不同的通知方式,如电子邮件或短信。

初始设计可能如下所示:

// 这个接口允许你将某些内容发送到指定的目的地。
public interface ISender
{
    void Send(Destination dest, Content content);
}

// 我们可以为不同的“发送策略”实现该接口:
public class PostalServiceSender : ISender { ... }
public class EmailNotifier : ISender { ... }

// 发货处理器根据配送策略(邮政、UPS、FedEx等)将实物订单发送给客户。
public class ShippingProcessor
{
    public ShippingProcessor(ISender shippingStrategy) { ... }
}

// 客户通知器根据通知策略(电子邮件、短信等)向客户发送订单状态更改的通知。
public class CustomerNotifier
{
    public CustomerNotifier(ISender notificationStrategy) { ... }
}

当你在 Autofac 中进行注册时,可能会像这样:

var builder = new ContainerBuilder();
builder.RegisterType<PostalServiceSender>().As<ISender>();
builder.RegisterType<EmailNotifier>().As<ISender>();
builder.RegisterType<ShippingProcessor>();
builder.RegisterType<CustomerNotifier>();
var container = builder.Build();

问题来了:如何确保发货处理器使用邮政服务策略,而客户通知器使用电子邮件策略?

方法 1:重新设计接口

当你遇到多个组件实现相同服务但不能同等对待的情况时,这通常是一个接口设计问题

从面向对象的角度来看,你应该遵循 里氏替换原则,这种情况就违反了这一原则。

换个角度看,假设我们有一个动物类的简单例子。有各种各样的动物,我们想创建一个专门表示鸟笼的特殊类,它只能容纳小型鸟类:

public abstract class Animal
{
    public abstract string MakeNoise();
    public abstract AnimalSize Size { get; }
}

public enum AnimalSize
{
    Small, Medium, Large
}

public class HouseCat : Animal
{
    public override string MakeNoise() { return "Meow!"; }
    public override AnimalSize { get { return AnimalSize.Small; } }
}

public abstract class Bird : Animal
{
    public override string MakeNoise() { return "Chirp!"; }
}

public class Parakeet : Bird
{
    public override AnimalSize { get { return AnimalSize.Small; } }
}

public class BaldEagle : Bird
{
    public override string MakeNoise() { return "Screech!"; }
    public override AnimalSize { get { return AnimalSize.Large; } }
}

如果直接设计鸟笼类,可能会这样:

public class BirdCage
{
    public BirdCage(Animal animal)
    {
        if (!(animal is Bird) || animal.Size != AnimalSize.Small)
        {
            // 我们只支持小型鸟类。
            throw new NotSupportedException();
        }
    }
}

**让鸟笼接受任何动物的设计并不合理。**至少应该限制为 “鸟”:

public class BirdCage
{
    public BirdCage(Bird bird)
    {
        if (bird.Size != AnimalSize.Small)
        {
            // 知道它是鸟,但需要是小型鸟。
            throw new NotSupportedException();
        }
    }
}

通过稍微调整设计,我们可以使其更容易,并且只允许正确类型的鸟被使用:

// 保留基类Bird...
public abstract class Bird : Animal
{
    public override string MakeNoise() { return "Chirp!"; }
}

// 并添加一个“宠物鸟”类——用于小型宠物鸟。
public abstract class PetBird : Bird
{
    // 封装方法以确保所有宠物鸟都是小型的。
    public sealed override AnimalSize { get { return AnimalSize.Small; } }
}

// 画眉是宠物鸟,所以我们将其基类改为PetBird。
public class Parakeet : PetBird { }

// 白头鹰一般不是宠物,所以不更改基类。
public class BaldEagle : Bird
{
    public override string MakeNoise() { return "Screech!"; }
    public override AnimalSize { get { return AnimalSize.Large; } }
}

现在我们可以很容易地设计出只支持小型宠物鸟的鸟笼。我们只需在构造函数中使用正确的基类:

public class BirdCage
{
    public BirdCage(PetBird bird) { }
}

这个例子虽然有些牵强附会,但原则依然适用——通过重新设计接口,我们可以确保鸟笼只接收预期的东西,而不会接收其他内容。

回到订单处理系统,虽然每个配送机制看起来只是“发送某物”,但其实它们发送的是不同类型的东西。也许有一个基础接口用于一般的“发送”,但可能需要一个中间层来区分发送的不同类型:

// 如果想要保留ISender接口,可以这样做...
public interface ISender
{
    void Send(Destination dest, Content content);
}

// 但为了区分发送的不同类型,我们可以添加中间接口,即使它们只是“标记”。
public interface IOrderSender : ISender { }
public interface INotificationSender : ISender { }

// 改变策略,根据它们允许发送的内容来实现适当的接口。
public class PostalServiceSender : IOrderSender { ... }
public class EmailNotifier : INotificationSender { ... }

// 最后,更新使用发送策略的类,只允许使用合适的策略。
public class ShippingProcessor
{
    public ShippingProcessor(IOrderSender shippingStrategy) { ... }
}

public class CustomerNotifier
{
    public CustomerNotifier(INotificationSender notificationStrategy) { ... }
}

通过重新设计接口,我们无需“根据上下文选择依赖项” ——我们使用类型来区分,并利用自动绑定的魔力在 解析过程 中发生。

如果能对解决方案进行调整,这是推荐的方法。

方法 2:修改注册

在 Autofac 中注册组件时,你可以使用 lambda 表达式而不是类型。你可以手动在那个上下文中将适当的类型与消费组件关联起来:

var builder = new ContainerBuilder();
builder.Register(ctx => new ShippingProcessor(new PostalServiceSender()));
builder.Register(ctx => new CustomerNotifier(new EmailNotifier()));
var container = builder.Build();

如果你想让发送者由 Autofac 自动解决,你可以同时暴露它们作为接口类型和它们自身的类型,然后在 lambda 表达式中解决它们:

var builder = new ContainerBuilder();

// 添加"AsSelf"子句,使这些组件既可以作为ISender接口,也可以作为它们自然的类型。
builder.RegisterType<PostalServiceSender>()
       .As<ISender>()
       .AsSelf();
builder.RegisterType<EmailNotifier>()
       .As<ISender>()
       .AsSelf();

// Lambda注册基于具体类型,而不是ISender接口。
builder.Register(ctx => new ShippingProcessor(ctx.Resolve<PostalServiceSender>()));
builder.Register(ctx => new CustomerNotifier(ctx.Resolve<EmailNotifier>()));
var container = builder.Build();

如果使用 lambda 机制感觉太 “手动” ,或者处理器对象需要大量参数,你可以 手动为注册项附加参数

var builder = new ContainerBuilder();

// 保持"AsSelf"子句。
builder.RegisterType<PostalServiceSender>()
       .As<ISender>()
       .AsSelf();
builder.RegisterType<EmailNotifier>()
       .As<ISender>()
       .AsSelf();

// 将已解决的参数附加到注册项上,以覆盖Autofac仅根据ISender参数的查找。
builder.RegisterType<ShippingProcessor>()
       .WithParameter(
         new ResolvedParameter(
           (pi, ctx) => pi.ParameterType == typeof(ISender),
           (pi, ctx) => ctx.Resolve<PostalServiceSender>()));
builder.RegisterType<CustomerNotifier>();
       .WithParameter(
         new ResolvedParameter(
           (pi, ctx) => pi.ParameterType == typeof(ISender),
           (pi, ctx) => ctx.Resolve<EmailNotifier>()));
var container = builder.Build();

使用参数方法,你在创建发送者和处理器时仍能获得 “自动绑定” 的好处,但在那些特定情况下,你可以指定非常具体的重写。

如果你无法更改接口并且希望保持简单,这是推荐的方法。

方法 3:使用键控服务

或许你可以修改注册,但你也使用了 模块 来注册许多不同的组件,无法通过类型直接将它们关联在一起。解决这个问题的一个简单方法是使用 键控服务

在这种情况下,Autofac 允许你为服务注册分配一个 “键” 或 “名称” ,并在另一个注册中根据该键进行解析。在注册发送者的模块中,你会为每个发送者分配适当的键;在注册处理器的模块中,你会在注册项中应用参数,以获取适当的键依赖项。

在注册发送器的模块中添加键名称:

public class SenderModule : Module
{
    protected override void Load(ContainerBuilder builder)
    {
        builder.RegisterType<PostalServiceSender>()
               .As<ISender>()
               .Keyed<ISender>("order");
        builder.RegisterType<EmailNotifier>()
               .As<ISender>()
               .Keyed<ISender>("notification");
    }
}

在注册处理器的模块中,添加使用已知键的参数:

public class ProcessorModule : Module
{
    protected override void Load(ContainerBuilder builder)
    {
        builder.RegisterType<ShippingProcessor>()
               .WithParameter(
                   new ResolvedParameter(
                       (pi, ctx) => pi.ParameterType == typeof(ISender),
                       (pi, ctx) => ctx.ResolveKeyed<ISender>("order")));
        builder.RegisterType<CustomerNotifier>();
               .WithParameter(
                   new ResolvedParameter(
                       (pi, ctx) => pi.ParameterType == typeof(ISender),
                       (pi, ctx) => ctx.ResolveKeyed<ISender>("notification")));
    }
}

现在,当处理器被解析时,它们会搜索键化的服务注册,并注入正确的实现。

你可以为同一个键有多个服务,所以如果你的发送器通过隐式支持的关系接受 IEnumerable<ISender> ,这种情况也可以工作。只需在处理器注册中将参数设置为 ctx.ResolveKeyed<IEnumerable<ISender>>("order") ,并使用适当的键注册每个发送器。

如果你有能力更改注册,并且不必对所有注册进行程序集扫描,这是推荐的选择。

选项 4:使用元数据

如果你需要比 键控服务 更灵活的东西,或者你没有能力直接影响注册,你可能想要考虑使用 注册元数据 功能将合适的服务连接在一起。

你可以直接为注册关联元数据:

public class SenderModule : Module
{
    protected override void Load(ContainerBuilder builder)
    {
        builder.RegisterType<PostalServiceSender>()
               .As<ISender>()
               .WithMetadata("SendAllowed", "order");
        builder.RegisterType<EmailNotifier>()
               .As<ISender>()
               .WithMetadata("SendAllowed", "notification");
    }
}

然后,你可以在消费者注册上使用元数据作为参数:

public class ProcessorModule : Module
{
    protected override void Load(ContainerBuilder builder)
    {
        builder.RegisterType<ShippingProcessor>()
               .WithParameter(
                   new ResolvedParameter(
                       (pi, ctx) => pi.ParameterType == typeof(ISender),
                       (pi, ctx) => ctx.Resolve<IEnumerable<Meta<ISender>>>()
                                   .First(a => a.Metadata["SendAllowed"].Equals("order")).Value));
        builder.RegisterType<CustomerNotifier>();
               .WithParameter(
                   new ResolvedParameter(
                       (pi, ctx) => pi.ParameterType == typeof(ISender),
                       (pi, ctx) => ctx.Resolve<IEnumerable<Meta<ISender>>>()
                                   .First(a => a.Metadata["SendAllowed"].Equals("notification")).Value));
    }
}

(是的,这比使用键化服务稍微复杂一些,但你可能希望利用 元数据设施提供的灵活性 。)

**如果你无法更改发送者组件的注册,但允许更改对象定义,**你可以使用“属性元数据”机制向组件添加元数据。首先,创建自定义元数据属性:

[MetadataAttribute]
public class SendAllowedAttribute : Attribute
{
    public string SendAllowed { get; set; }

    public SendAllowedAttribute(string sendAllowed)
    {
        this.SendAllowed = sendAllowed;
    }
}

然后,你可以将自定义元数据属性应用于发送者组件:

[SendAllowed("order")]
public class PostalServiceSender : IOrderSender { ... }

[SendAllowed("notification")]
public class EmailNotifier : INotificationSender { ... }

当你注册发送者时,请确保注册AttributedMetadataModule

public class SenderModule : Module
{
    protected override void Load(ContainerBuilder builder)
    {
        builder.RegisterType<PostalServiceSender>().As<ISender>();
        builder.RegisterType<EmailNotifier>().As<ISender>();
        builder.RegisterModule<AttributedMetadataModule>();
    }
}

消费组件可以像平常一样使用元数据——属性属性的名称将成为元数据中的名称:

public class ProcessorModule : Module
{
    protected override void Load(ContainerBuilder builder)
    {
        builder.RegisterType<ShippingProcessor>()
               .WithParameter(
                   new ResolvedParameter(
                       (pi, ctx) => pi.ParameterType == typeof(ISender),
                       (pi, ctx) => ctx.Resolve<IEnumerable<Meta<ISender>>>()
                                   .First(a => a.Metadata["SendAllowed"].Equals("order"))));
        builder.RegisterType<CustomerNotifier>()
               .WithParameter(
                   new ResolvedParameter(
                       (pi, ctx) => pi.ParameterType == typeof(ISender),
                       (pi, ctx) => ctx.Resolve<IEnumerable<Meta<ISender>>>()
                                   .First(a => a.Metadata["SendAllowed"].Equals("notification"))));
    }
}

对于你的消费组件,如果你不介意在参数定义中添加自定义 Autofac 属性,也可以使用属性元数据:

public class ShippingProcessor
{
    public ShippingProcessor([WithMetadata("SendAllowed", "order")] ISender shippingStrategy) { ... }
}

public class CustomerNotifier
{
    public CustomerNotifier([WithMetadata("SendAllowed", "notification")] ISender notificationStrategy) { ... }
}

如果你的消费组件使用了属性,你需要使用 WithAttributeFilter 注册它们:

public class ProcessorModule : Module
{
    protected override void Load(ContainerBuilder builder)
    {
        builder.RegisterType<ShippingProcessor>().WithAttributeFilter();
        builder.RegisterType<CustomerNotifier>().WithAttributeFilter();
    }
}

再次强调,元数据机制非常灵活。你可以混合搭配将元数据与组件和服务消费者关联的方式——属性、参数等。有关 注册元数据注册参数解析参数隐式支持的关系 (如 Meta<T> 关系)的信息,请参阅各自页面。

如果你已经使用元数据或需要元数据提供的灵活性,这是推荐的选择。

在本文档中