【问题标题】:C# Unity - change concrete implementation at runtimeC# Unity - 在运行时更改具体实现
【发布时间】:2018-01-20 17:32:56
【问题描述】:

我在理解 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
  }
}

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

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

【问题讨论】:

标签: c# dependency-injection inversion-of-control unity-container


【解决方案1】:

你的问题很公平。基于一些用户输入或配置设置在运行时解决依赖关系是一个众所周知的问题。 Mark Seemann 在他的好书Dependency Injection in .NET 的单独部分专门讨论了这个问题 - “6.1 将运行时值映射到抽象”。 我不能完全同意 NightOwl888 这不是 DI 问题。

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

马克·西曼在他的书中描述的第一个是让工厂将所选实现的指示作为参数。以下是其工作原理的基本说明。

首先,您应该以某种方式传递您想要使用的实现。它可能只是一个字符串(例如“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")));

Sender 的创建方式如下:

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

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

【讨论】:

  • 我希望可以提出这个较旧的线程。我遇到了同样的问题,但是使用 MVVM 的 WPF + EF Core,我需要在运行时更改连接字符串。我了解您的解决方案,并且我想我知道如何为自己实施它 - 但我应该在哪里更改它?我在 App.xaml.cs 中注册我的容器,但是我需要在 ViewModel 中更改它,对吗?我应该如何在那里获得该容器实例,以便我可以使用 Resolve()?提前谢谢!
猜你喜欢
  • 1970-01-01
  • 2015-01-15
  • 2015-08-28
  • 2013-12-11
  • 1970-01-01
  • 2012-04-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多