4

出于本次讨论的目的,对象构造函数可能采用两种参数:状态依赖或服务依赖。使用 IOC 容器提供服务依赖项很容易:DI 接管。但相比之下,状态依赖通常只有客户端知道。即对象请求者。

事实证明,让客户端通过 IOC 容器提供状态参数是非常痛苦的。我将展示几种不同的方法来做到这一点,所有这些方法都有很大的问题,并询问社区是否还有我遗漏的其他选择。让我们开始:

在我将 IOC 容器添加到我的项目代码之前,我从一个这样的类开始:

class Foobar {
   //parameters are state dependencies, not service dependencies
   public Foobar(string alpha, int omega){...};
   //...other stuff
}

我决定向 Foobar 类添加一个 Logger 服务依赖关系,也许我会通过 DI 提供:

class Foobar {
    public Foobar(string alpha, int omega, ILogger log){...};
    //...other stuff
}

但后来我也被告知我需要让类 Foobar 本身“可交换”。也就是说,我需要对 Foobar 实例进行服务定位。我在组合中添加了一个新界面:

class Foobar : IFoobar {
    public Foobar(string alpha, int omega, ILogger log){...};
    //...other stuff
}

当我进行服务定位器调用时,它将为我提供 ILogger 服务依赖项。不幸的是,状态依赖项 Alpha 和 Omega 并非如此。一些容器提供了一种语法来解决这个问题:

//Unity 2.0 pseudo-ish code:
myContainer.Resolve<IFoobar>(
   new parameterOverride[] { {"alpha", "one"}, {"omega",2} } 
);

我喜欢这个功能,但我不喜欢它是无类型的,并且对开发人员来说必须传递哪些参数(通过智能感知等)并不明显。所以我看另一个解决方案:

//This is a "boiler plate" heavy approach!
class Foobar : IFoobar {
   public Foobar (string alpha, int omega){...};
   //...stuff
}
class FoobarFactory : IFoobarFactory {
   public IFoobar IFoobarFactory.Create(string alpha, int omega){
      return new Foobar(alpha, omega);
   }
}

//fetch it...
myContainer.Resolve<IFoobarFactory>().Create("one", 2);

以上解决了类型安全和智能感知问题,但它(1)强制 Foobar 类通过服务定位器而不是 DI 获取 ILogger 并且(2)它需要我制作一堆样板(XXXFactory,IXXXFactory)对于我可能使用的各种 Foobar 实现。如果我决定采用纯粹的服务定位器方法,它可能不是问题。但我仍然无法忍受完成这项工作所需的所有样板。

所以我尝试了另一种容器提供的支持:

//overall, this is a pseudo-memento pattern.
class Foobar : IFoobar {
   public Foobar (FoobarParams parms){
      this.alpha = parms.alpha;
      this.omega = parms.omega;
   };
   //...stuff
}

class FoobarParams{
   public FoobarParams(string alpha, int omega){...};
}

//fetch an instance:
FoobarParams parms = new FoobarParams("one",2);
//Unity 2.0 pseudo-code...
myContainer.resolve<IFoobar>(new parameterOverride(parms) );

通过这种方法,我已经恢复了一半的智能。但是我必须等到运行时才能检测到我可能忘记提供“FoobarParams”参数的错误。

所以让我们尝试一种新方法:

//code named "concrete creator"
class Foobar : IFoobar {
    public Foobar(string alpha, int omega, ILogger log){...};
    static IFoobar Create(string alpha, int omega){
       //unity 2.0 pseudo-ish code.  Assume a common
       //service locator, or singleton holds the container...
       return Container.Resolve<IFoobar>(
           new parameterOverride[] {{"alpha", alpha},{"omega", omega} } 
       );
    }

//Get my instance:
Foobar.Create("alpha",2);

我实际上不介意我使用具体的“Foobar”类来创建 IFoobar。它代表了一个我不希望在我的代码中改变的基本概念。我也不介意静态“创建”中缺乏类型安全,因为它现在被封装了。我的智能感知也在工作!如果它们不适用(Unity 2.0 行为),以这种方式创建的任何具体实例都将忽略提供的状态参数。也许一个不同的具体实现“FooFoobar”可能有一个正式的 arg 名称不匹配,但我仍然很满意。

但这种方法的一个大问题是它只能在 Unity 2.0 中有效(结构图中的不匹配参数会引发异常)。所以只有我留在 Unity 才好。问题是,我开始越来越喜欢 Structure Map。所以现在我进入另一个选择:

class Foobar : IFoobar, IFoobarInit {
   public Foobar(ILogger log){...};

   public IFoobar IFoobarInit.Initialize(string alpha, int omega){
      this.alpha = alpha; 
      this.omega = omega;
      return this;
   }
}

//now create it...     
IFoobar foo = myContainer.resolve<IFoobarInit>().Initialize("one", 2)

现在有了这个,我对其他方法有了一个很好的折衷:(1)我的论点是类型安全/智能感知的(2)我可以选择通过 DI(如上所示)或服务定位器获取 ILogger,( 3)不需要制作一个或多个单独的具体 FoobarFactory 类(与前面冗长的“样板”示例代码对比),并且(4)它合理地维护了“使接口易于正确使用,难以正确使用”的原则使用不当。” 至少可以说它并不比前面讨论的替代方案差。

一个接受障碍仍然存在:我还想申请“按合同设计”。

我提供的每个示例都有意支持构造函数注入(用于状态依赖项),因为我想保留最常用的“不变”支持。即,不变量在构造函数完成时建立。

在上面的示例中,当对象构造完成时,不变量没有建立。只要我在做本土的“合同设计”,我就可以告诉开发人员在调用 Initialize(...) 方法之前不要测试不变量。

但更重要的是,当 .net 4.0 出来时,我想使用它的“代码契约”支持按契约进行设计。根据我的阅读,它与最后一种方法不兼容。

诅咒!

当然,我也想到我的整个哲学都是错误的。也许有人告诉我,通过服务定位器召唤一个 Foobar : IFoobar 意味着它是一个服务 - 服务只有其他服务依赖关系,它们没有状态依赖关系(例如这些示例中的 Alpha 和 Omega)。我也乐于聆听此类哲学问题,但我也想知道阅读哪些半权威性参考资料会引导我走上这条思想道路。

所以现在我把它转向社区。我应该考虑什么方法我还没有?我必须真的相信我已经用尽了我的选择吗?

ps 这种问题,连同其他问题,让我相信整个 IOC Container 的想法,至少在 .net 中,还为时过早。它让我想起了人们会站在他们的头上让“C”语言感觉面向对象的日子(添加奇怪的宏等)。我们应该寻找的是 IOC Containers 的 CLR 和语言支持。例如,想象一种称为“initiate”的新型接口。启动类似于接口,但也需要特定的构造函数签名。剩下的我留给学生做练习......

4

2 回答 2

3

您正在寻找的是“工厂适配器”或 Func<T, U>。

需要动态创建另一个组件的参数化实例的组件使用 Func<T, U> 依赖类型来表示:

class Bar
{
    Func<string, int, IFoo> _fooCreator;

    public Bar(Func<string, int, IFoo> fooCreator) {
        _fooCreator = fooCreator;
    }

    public void Go() {
        var foo = _fooCreator("a", 42);
        // ...
    }
}

例如,Autofac 将在注册 IFoo 时自动提供 Func。它还将参数合并到 Foo 构造函数(包括 ILogger)中,并丢弃任何不必要的参数,而不是抛出错误。

Autofac 还支持自定义委托,例如:

delegate IFoo FooCreator(string alpha, int omega);

这样 Bar 可以重写:

class Bar
{
    FooCreator _fooCreator;

    public Bar(FooCreator fooCreator) {
        _fooCreator = fooCreator;
    }

    public void Go() {
        var foo = _fooCreator("a", 42);
        // ...
    }
}

当使用自定义委托时,参数将按名称匹配,而不是像 Func 那样按类型匹配。

Autofac 有一些您可以查看的文档:http ://code.google.com/p/autofac/wiki/DelegateFactories 。

在多个 IOC 容器社区之间有一个名为“Common Context Adapters”的协作项目,用于标准化这些和其他高阶依赖类型。项目站点位于http://cca.codeplex.com

CCA 提出的第一个规范涵盖了工厂适配器,您可以在此处阅读:http ://cca.codeplex.com/wikipage?title=FuncFactoryScenarios&referringTitle=Documentation 。

您可能会发现其他一些有用的链接:

http://nblumhardt.com/2010/04/introducing-autofac-2-1-rtw/ http://nblumhardt.com/2010/01/the-relationship-zoo/

我希望你会发现,虽然 IoC 容器在完全透明之前还有很长的路要走,但我们实际上正在努力实现这一目标:)

缺口

于 2010-04-17T08:56:30.473 回答
0

如果这些参数在应用程序的生命周期内保持不变,那么您可以添加一个 IConfigurationService,其唯一目的是将这些参数返回给需要它们的任何人。IConfigurationService 的实现可能具有硬编码的值,从配置文件中读取它们......无论如何。当然 IConfigurationService 的实现是通过 IoC 容器获取的。

如果这些参数因实例而异,那么我认为不应将它们作为 IoC 容器加载的对象的构造函数参数提供。这使得您的所有组件都需要查找/依赖于 IoC 容器,这首先破坏了 IoC 容器的意义。

要么通过公开一个 setter 方法使它们在对象本身上可配置,(当它们可能在对象的生命周期内发生变化时这是合适的),或者将它们作为返回对象的工厂方法的参数(工厂对象在你的IoC 容器)。

您对使用工厂犹豫不决,但我认为这是一种优雅的方法。是的,创建工厂需要付出努力,但是由于需要支持该行为,因此它不是代码膨胀。它是一个满足要求的简单模式。

于 2010-04-16T23:51:23.850 回答