C# Unity - 在运行时更改具体实现

C# Unity - change concrete implementation at runtime

我在思考 IoC 时遇到了一些麻烦 - 特别是使用 Unity。

假设我有一个要用来发送电子邮件的应用程序。我会这样建模:

public interface IEmailSender
{
  void SendEmail();
}

然后创建接口的一些实现:

public class GmailEmailSender : IEmailSender
{
  public void SendEmail()
  {
      //code to send email using Gmail
  }
}

public class YahooEmailSender : IEmailSender
{
  public void SendEmail()
  {
      //code to send email using Yahoo
  }
}

我还有一个 class 可以实际发送电子邮件

public class EmailSender
{
   IEmailSender _emailSender;
   public EmailSender(IEmailSender emailSender)
   {
       _emailSender= emailSender;
   }

   public void Send()
   {
      _emailSender.SendEmail();
   }
}

所以我了解了如何将 Unity 配置为始终使用其中一种实现:

IUnityContainer container = new UnityContainer().RegisterType<IEmailSender, GmailEmailSender>());

但我不太明白的是,如果我想根据某些标准选择 YahooEmailSender,并根据其他标准选择 GmailEmailSender,我应该在哪里编写逻辑来做出决定,然后注入适当的EmailSender 构造函数的具体实现,不使用

EmailSender emailSender = new EmailSender(new YahooEmailSender());

你的问题很公平。基于某些用户输入或配置设置在运行时解决依赖关系是一个众所周知的问题。 Mark Seemann 在他的巨著 Dependency Injection in .NET 的单独章节中专门讨论了这个问题——“6.1 将运行时值映射到抽象”。 而且我不能完全同意 NightOwl888 的观点,即这不是 DI 问题。

这个问题有两个主要的解决方案。

Mark Seeman 在他的书中描述的第一个方法是让工厂将所选实现的指示作为参数。这是它如何工作的基本描述。

首先,你应该以某种方式传递你想要使用的实现。它可以只是一个字符串(例如 "gmail"、"yahoo"),但最好通过 enum:

来完成
public enum EmailTarget
{
    Gmail,
    Yahoo,
}

那么你应该自己定义工厂。通常,它看起来像这样:

public interface IEmailSenderFactory
{
    IEmailSender CreateSender(EmailTarget emailTarget);
}

那么你应该为工厂提供实现。它可以像这样简单:

public class EmailSenderFactory : IEmailSenderFactory
{
    public IEmailSender CreateSender(EmailTarget emailTarget)
    {
        switch (emailTarget)
        {
            case EmailTarget.Gmail:
                return new GmailEmailSender();

            case EmailTarget.Yahoo:
                return new YahooEmailSender();

            default:
                throw new InvalidOperationException($"Unknown email target {emailTarget}");
        }
    }
}

然而,在更复杂的情况下,IEmailSender 的实例也应该通过 DI 容器创建。在这种情况下,您可以使用基于 IUnityContainer:

的工厂
public class EmailSenderFactory : IEmailSenderFactory
{
    private readonly IUnityContainer diContainer;

    public EmailSenderFactory(IUnityContainer diContainer)
    {
        this.diContainer = diContainer;
    }

    public IEmailSender CreateSender(EmailTarget emailTarget)
    {
        switch (emailTarget)
        {
            case EmailTarget.Gmail:
                return diContainer.Resolve<GmailEmailSender>();

            case EmailTarget.Yahoo:
                return diContainer.Resolve<YahooEmailSender>();

            default:
                throw new InvalidOperationException($"Unknown email target {emailTarget}");
        }
    }
}

那么你应该调整EmailSender并在其中注入IEmailSenderFactorySend() 方法应使用指定选定发件人的 EmailTarget 枚举的值进行扩展:

public class EmailSender
{
    private readonly IEmailSenderFactory senderFactory;

    public EmailSender(IEmailSenderFactory senderFactory)
    {
        this.senderFactory = senderFactory;
    }

    public void Send(EmailTarget emailTarget)
    {
        var sender = senderFactory.CreateSender(emailTarget);
        sender.SendEmail();
    }
}

最后一件事是适当的组合根:

IUnityContainer container = new UnityContainer();
container.RegisterType<IEmailSender, GmailEmailSender>("gmail");
container.RegisterType<IEmailSender, YahooEmailSender>("yahoo");
container.RegisterType<IEmailSenderFactory, EmailSenderFactory>();

最后当您需要发送电子邮件时:

var sender = container.Resolve<EmailSender>();
sender.Send(EmailTarget.Gmail);

第二种方法更简单。它不使用工厂,而是基于 Unity 命名依赖项。通过这种方法,您的 类 可以保持原样。这是组合根:

IUnityContainer container = new UnityContainer();
container.RegisterType<IEmailSender, GmailEmailSender>("gmail");
container.RegisterType<IEmailSender, YahooEmailSender>("yahoo");
container.RegisterType<EmailSender>("gmail", new InjectionConstructor(new ResolvedParameter<IEmailSender>("gmail")));
container.RegisterType<EmailSender>("yahoo", new InjectionConstructor(new ResolvedParameter<IEmailSender>("yahoo")));

发件人的创建方式如下:

var emailSender = container.Resolve<EmailSender>("gmail");
emailSender.Send();

由您决定使用这些方法中的哪一种。纯粹主义者会说第一个更好,因为您没有将特定的 DI 容器与您的应用程序逻辑混合。实际上你这样做,如果工厂是基于 DI 容器,但它集中在一个地方并且可以很容易地被替换。然而,第二种方法要简单得多,可以用于最简单的场景。