如何在没有全局静态服务的情况下实现IOC(非服务定位器解决方案)?

30

我们希望在Unity中使用IOC。我看到的实现方式是有一个全局静态服务(我们称之为IOCService),它持有对Unity容器的引用,该容器注册所有的接口/类组合,每个类都会请求该对象:给我一个Ithis或IThat的实现。

经常听到回应说这种模式不好,因为它导致所有类都依赖于IOCService(而不是Unity容器,因为它只在IOCService内部知道)。

但是我不经常看到的是: 有什么替代方法吗?

- Michel

编辑:发现全局静态服务被称为服务定位器,已将其添加到标题中。


开始悬赏,非常抱歉悬赏点数较少,我只赚了一点点 :-) - Michel
1
如果您想要更详细的了解整个主题,我目前正在撰写一本有关此书:http://www.manning.com/seemann/。 - Mark Seemann
5个回答

11

另一种选择是仅在应用程序的最高级别拥有一个容器实例,然后使用该容器来解析您需要在该层中创建的每个对象实例。

例如,大多数可执行文件的主方法看起来就像这样(减去异常处理):

private static void main(string[] args) {

     Container container = new Container();

     // Configure the container - by hand or via file

     IProgramLogic logic = container.Resolve<IProgramLogic>();

     logic.Run();
}

你的程序(在这里由IProgramLogic实例表示)不需要知道容器的任何信息,因为container.Resolve将创建所有它的依赖项 - 以及其依赖项的依赖项,一直到没有自己依赖项的叶类。


ASP.NET是一个更难的情况,因为Web Forms不支持构造函数注入。我通常在我的Web Forms应用程序中使用Model-View-Presenter,因此我的Page类只有一个依赖项 - 它们的Presenter。我不对它们进行单元测试(所有有趣和可测试的都在我的Presenter中,我进行测试),我也不替换Presenter。因此,我不与框架斗争 - 我只在我的HttpApplication类(在global.asax.cs中)上公开容器属性,并直接从我的Page文件中使用它:

protected void Page_Load(object sender, EventArgs args) {
    ICustomerPresenter presenter = Global.Container.Resolve<ICustomerPresenter>();
    presenter.Load();
}
那当然是服务定位器 - 虽然 Page 类是唯一与定位器相耦合的东西:您的 Presenter 和其所有依赖项仍然完全与您的 IoC 容器实现解耦。如果您的 Page 文件中有许多依赖项(即,如果您不使用 Model-View-Presenter),或者如果将您的 Page 类从您的 Global 应用程序类中解耦对您很重要,那么您应该尝试找到一个集成到 Web 表单请求管道中的框架,并使用属性注入(如下方评论中由 Nicholas 建议)-或编写自己的 IHttpModule 并手动执行属性注入。

我以非常相似的方式实现了这个。在我的情况下,Global是IoCContainer,而Container是GetInstance。这是整个应用程序中唯一手写单例的类(所有其他类都是单例或不是,取决于Unity配置)。这个类(Global/IoCContainer)位于一个名为Configuration的项目中。该项目仅被UI引用,并引用所有项目(除UI之外)以能够实例化适当的对象。 - bloparod
2
这仍然是一个经常被反对的模式(服务定位器)。即使在ASP.NET WebForms中,也可以通过属性注入(http://code.google.com/p/autofac/wiki/AspNetIntegration#Implementing_WebForms_Pages_and_User_Controls)或通过重构为基于Presenter的方法来实现依赖注入。 - Nicholas Blumhardt
我同意尼古拉斯的观点;除此之外,这个答案听起来很合理,但它仍然是一个服务定位器吗? - Michel
1
@Michael - 实际上它确实使用了服务定位器,但在我认为是可辩护的狭窄边界内(由于 asp.net Web Forms 对我们施加的限制)。我不知道是什么驱使我使用 asp.net 作为我的例子,因为它并不方便进行适当的依赖注入技术。我将尝试在今天稍后更新我的答案以更好地解释。 - Jeff Sternal

7

+1 是为了表明知道Service Locator是不好的事情

问题在于 - Unity并不是非常复杂,所以我不知道使用正确的方式进行IoC有多容易/困难。

我最近写了一些博客文章,您可能会发现它们很有用。


这些文章总结得非常好。Unity理解构造函数注入,因此第一篇文章可以直接翻译。据我所知,Unity不支持动态发射工厂,但是关于抽象工厂的使用模式仍然适用 - 您只需要手动实现具体的工厂,但这很容易做到。 - Mark Seemann
Unity 2.0 支持基于委托的自动工厂。 - Chris Tavares

5

不要直接使用容器,而是通过构造函数/属性注入隐式地使用它。创建一个核心类(或一组核心类),这些类依赖于应用程序的所有主要部分。

大多数容器都允许您在构造函数中放置ISomething[],并将所有ISomething实例注入到您的类中。

这样,当您引导应用程序时:

  1. 实例化您的容器
  2. 注册所有必需品
  3. 解析核心类(这将引入您需要的所有其他依赖项)
  4. 运行应用程序的“主”部分

现在,根据您编写的应用程序类型,有不同的策略可避免将IoC容器标记为“静态”。

对于ASP.NET Web应用程序,您可能最终会将容器存储在应用程序状态中。对于ASP.NET MVC应用程序,您需要更改控制器工厂。

对于桌面应用程序,情况变得更加复杂。 Caliburn 使用了一个有趣的解决方案,使用IResult构造(这适用于WPF应用程序,但也可以适应Windows Forms)。


Unity在构造函数解析中无法解析ISomething[]IEnumerable<ISomething>。如果需要传递已注册依赖项列表,则必须使用InjectionConstructor。这有点糟糕,因为这意味着这些依赖项在启动时只解析一次,因此不允许瞬态生命周期。如果有更好的解决方案,我会很高兴知道。 - Igor Zevaka
如果您需要在运行时将新实例注入到代码中,则应该有一个显式的方式来完成这个过程(不是由IoC容器注入)。您可以设置一个事件聚合器来通知特定类型的新实例何时到达。 - Garo Yeriazarian
@Igor - 你也可以通过在运行时注入一个可以创建新的瞬态实例的工厂来解决这个问题。 - Jeff Sternal

4

理论上,为了不必担心有一个静态的IoC实例,您需要遵循Fight Club Rule——即不要谈论打斗俱乐部——即不要提及IoC容器。

这意味着您的组件在很大程度上不应该了解IoC容器。只有在注册组件时才应使用它。如果一个类需要解析某些东西,它应该作为依赖项注入。

简单情况是足够简单的。如果PaymentService依赖于IAccount,则后者应由IoC注入:

interface IAccount {
  Deposit(int amount);
}

interface CreditCardAccount : IAccount {
  void Deposit(int amount) {/*implementation*/}
  int CheckBalance() {/*implementation*/}
}

class PaymentService {

  IAccount account;

  public PaymentService (IAccount account) {
    this.account = account;
  }

  public void ProcessPayment() {
    account.Deposit(5);
  }
}
//Registration looks something like this
container.RegisterType<IAccount, CreditCardAccount>();
container.RegisterType<PaymentService>();

不太简单的情况是您想要注入多个注册信息。特别是在执行任何形式的约定优于配置并从名称创建对象时,这种情况尤其适用。

以我们的付款示例为例,假设您想枚举所有帐户并检查它们的余额:

class PaymentService {

  IEnumerable<IAccount> accounts;

  public PaymentService (IEnumerable<IAccount> accounts) {
    this.accounts = accounts;
  }

  public void ProcessPayment() {
    foreach(var account in accounts) {
      account.Chackbalance();
    }
  }
}

Unity有能力将多个接口映射到同一类中(但它们必须具有不同的名称)。然而,它不会自动将这些已注册的接口集合注入到需要这些接口集合的类中。因此,上面的示例在运行时会抛出解析失败的异常。

如果您不关心这些对象是否永久存在,可以以更静态的方式注册PaymentService

container.RegisterType<PaymentService>(new InjectionConstructor(container.ResolveAll<IAccount>()));

以上代码将注册PaymentService并使用在注册时解析的IAccount实例集合。
另外,您还可以将容器实例作为依赖项传递,并让PaymentService执行帐户的解析。 这不完全遵循Fight Club规则,但比静态服务定位器更少臭味。
class PaymentService {

  IEnumerable<IAccount> accounts;

  public PaymentService (IUnityContainer container) {
    this.accounts = container.ResolveAll<IAccount>();
  }

  public void ProcessPayment() {
    foreach(var account in accounts) {
      account.Chackbalance();
    }
  }
}
//Registration is pretty clean in this case
container.RegisterType<IAccount, CreditCardAccount>();
container.RegisterType<PaymentService>();
container.RegisterInstance<IUnityContainer>(container);

2
如果您担心在整个应用程序中存在对Unity的依赖,则可以将服务定位器与门面组合使用以隐藏IOC实现。这样,您的应用程序不会对Unity产生依赖,只需要有“某物”可以为您解决类型。
例如:
public interface IContainer
{
    void Register<TAbstraction,TImplementation>();
    void RegisterThis<T>(T instance);
    T Get<T>();
}

public static class Container
{
    static readonly IContainer container;

    public static InitializeWith(IContainer containerImplementation)
    {
        container = containerImplementation;
    }

    public static void Register<TAbstraction, TImplementation>()
    {
        container.Register<TAbstraction, TImplementation>();
    }

    public static void RegisterThis<T>(T instance)
    {
        container.RegisterThis<T>(instance);
    }

    public static T Get<T>()
    {
        return container.Get<T>();
    }
}

现在你只需要一个适用于您IOC容器的IContainer实现。
public class UnityContainerImplementation : IContainer
{
    IUnityContainer container;

    public UnityContainerImplementation(IUnityContainer container)
    {
        this.container = container;
    }

    public void Register<TAbstraction, TImplementation>()
    {
        container.Register<TAbstraction, TImplementation>();
    }

    public void RegisterThis<T>(T instance)
    {
        container.RegisterInstance<T>(instance);
    }

    public T Get<T>()
    {
        return container.Resolve<T>();
    }
}

现在您拥有一个作为IOC服务外观的服务定位器,并且可以配置您的服务定位器以使用Unity或任何其他IOC容器。应用程序的其余部分不依赖于IOC实现。
要配置您的服务定位器:
IUnityContainer unityContainer = new UnityContainer();
UnityContainerImplementation containerImpl = new UnityContainerImplementation(unityContainer);
Container.InitializeWith(containerImpl);

为了测试,你可以创建一个IContainer的存根,返回任何你想要的内容,并使用它来初始化Container


5
对于许多人来说,服务定位器确实是问题所在,而不仅仅是对特定依赖注入容器的具体依赖。Mark Seemann在此处非常清楚地阐述了服务定位器的问题。链接:http://blog.ploeh.dk/2010/02/03/ServiceLocatorIsAnAntiPattern.aspx。 - Jeff Sternal
确实是很好的代码,但很多人对服务定位器模式有意见(因为它会创建对服务定位器类的依赖,而不仅仅是Unity类),但问题在于我从未见过一个好的替代方案,这就是为什么我提出了这个问题。 - Michel
@Jeff:你的链接里有非常好的文章。 - Michel

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接