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

Mil*_*nMo 1 c# dependency-injection inversion-of-control unity-container

我在 IoC 方面遇到了一些麻烦 - 特别是使用 Unity。

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

public interface IEmailSender
{
  void SendEmail();
}
Run Code Online (Sandbox Code Playgroud)

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

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
  }
}
Run Code Online (Sandbox Code Playgroud)

我还有一门课来实际发送电子邮件

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

   public void Send()
   {
      _emailSender.SendEmail();
   }
}
Run Code Online (Sandbox Code Playgroud)

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

IUnityContainer container = new UnityContainer().RegisterType<IEmailSender, GmailEmailSender>());
Run Code Online (Sandbox Code Playgroud)

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

EmailSender emailSender = new EmailSender(new YahooEmailSender());
Run Code Online (Sandbox Code Playgroud)

Cod*_*ler 6

你的问题很公平。根据一些用户输入或配置设置在运行时解决依赖关系是一个众所周知的问题。Mark Seemann 在他的伟大著作《.NET 中的依赖注入》中用单独的部分来解决这个问题——“6.1 将运行时值映射到抽象”。我不能完全同意 NightOwl888 这不是 DI 问题。

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

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

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

public enum EmailTarget
{
    Gmail,
    Yahoo,
}
Run Code Online (Sandbox Code Playgroud)

然后你应该定义工厂本身。通常,它看起来像这样:

public interface IEmailSenderFactory
{
    IEmailSender CreateSender(EmailTarget emailTarget);
}
Run Code Online (Sandbox Code Playgroud)

然后你应该为工厂提供实现。它可以很简单:

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}");
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

但是,在更复杂的情况下,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}");
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

然后你应该调整EmailSender并注入IEmailSenderFactory它。Send()方法应使用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();
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

var sender = container.Resolve<EmailSender>();
sender.Send(EmailTarget.Gmail);
Run Code Online (Sandbox Code Playgroud)

第二种方法更简单。它不使用工厂,而是基于 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")));
Run Code Online (Sandbox Code Playgroud)

Sender 的创建方式如下:

var emailSender = container.Resolve<EmailSender>("gmail");
emailSender.Send();
Run Code Online (Sandbox Code Playgroud)

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