IOC 容器处理非默认构造函数中的状态参数

发布于 2024-08-29 04:52:35 字数 4408 浏览 10 评论 0原文

出于本次讨论的目的,对象构造函数可能采用两种参数:状态依赖项或服务依赖项。使用 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
}

当我调用服务定位器时,它将为我DI 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)它需要我制作一堆锅炉-plate (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。它代表了一个基本概念,我不希望在我的代码中改变它。我也不介意静态“Create”中缺乏类型安全,因为它现在已被封装。我的智能感知也正常工作了!如果提供的状态参数不适用,以这种方式创建的任何具体实例都会忽略它们(Unity 2.0 行为)。也许不同的具体实现“FooFoobar”可能有正式的参数名称不匹配,但我仍然对此感到非常满意。

但这种方法的一个大问题是,它仅适用于 Unity 2.0(结构图中参数不匹配将引发异常)。所以只有我留在 Unity 才好。问题是,我开始更喜欢结构图了。所以现在我开始另一个选择:

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)它合理地维护了“make界面易于正确使用,但很难错误使用。”至少可以说它并不比之前讨论的替代方案更差。

但仍然存在一个接受障碍:我还想应用“合同设计”。

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

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

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

诅咒!

当然,我也意识到我的整个哲学已经偏离了。也许我会被告知,通过服务定位器召唤 Foobar : IFoobar 意味着它是一个服务 - 并且服务仅具有其他服务依赖项,它们没有状态依赖项(例如这些示例中的 Alpha 和 Omega)。我也愿意聆听此类哲学问题,但我也想知道哪些半权威的参考文献可以引导我走上这条思考之路。

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

ps 这种问题,与其他问题一起,促使我相信整个IOC容器的想法,至少在.net 中,现在还为时过早。这让我想起了人们倒立着让“C”语言感觉面向对象的日子(添加奇怪的宏等)。我们应该寻找的是 IOC 容器的 CLR 和语言支持。例如,想象一种称为“启动”的新型界面。启动的工作方式类似于接口,但也需要特定的构造函数签名。剩下的我留给学生作为练习......

For the purpose of this discussion, there are two kinds of parameters an object constructor might take: state dependency or service dependency. Supplying a service dependency with an IOC container is easy: DI takes over. But in contrast, state dependencies are usually only known to the client. That is, the object requestor.

It turns out that having a client supply the state params through an IOC Container is quite painful. I will show several different ways to do this, all of which have big problems, and ask the community if there is another option I'm missing. Let's begin:

Before I added an IOC container to my project code, I started with a class like this:

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

I decide to add a Logger service depdendency to the Foobar class, which perhaps I'll provide through DI:

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

But then I'm also told I need to make class Foobar itself "swappable." That is, I'm required to service-locate a Foobar instance. I add a new interface into the mix:

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

When I make the service locator call, it will DI the ILogger service dependency for me. Unfortunately the same is not true of the state dependencies Alpha and Omega. Some containers offer a syntax to address this:

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

I like the feature, but I don't like that it is untyped and not evident to the developer what parameters must be passed (via intellisense, etc). So I look at another solution:

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

The above solves the type-safety and intellisense problem, but it (1) forced class Foobar to fetch an ILogger through a service locator rather than DI and (2) it requires me to make a bunch of boiler-plate (XXXFactory, IXXXFactory) for all varieties of Foobar implementations I might use. Should I decide to go with a pure service locator approach, it may not be a problem. But I still can't stand all the boiler-plate needed to make this work.

So then I try another variation of container-provided support:

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

With this approach, I've half-way recovered my intellisense. But I must wait till run-time to detect the error where I might forget to supply the "FoobarParams" parameter.

So let's try a new approach:

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

I actually don't mind that I'm using the concrete "Foobar" class to create an IFoobar. It represents a base concept that I don't expect to change in my code. I also don't mind the lack of type-safety in the static "Create", because it is now encapsulated. My intellisense is working too! Any concrete instance made this way will ignore the supplied state params if they don't apply (a Unity 2.0 behavior). Perhaps a different concrete implementation "FooFoobar" might have a formal arg name mismatch, but I'm still pretty happy with it.

But the big problem with this approach is that it only works effectively with Unity 2.0 (a mismatched parameter in Structure Map will throw an exception). So it is good only if I stay with Unity. The problem is, I'm beginning to like Structure Map a lot more. So now I go onto yet another option:

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)

Now with this I've got a somewhat nice compromise with the other approaches: (1) My arguments are type-safe / intellisense aware (2) I have a choice of fetching the ILogger via DI (shown above) or service locator, (3) there is no need to make one or more seperate concrete FoobarFactory classes (contrast with the verbose "boiler-plate" example code earlier), and (4) it reasonably upholds the principle "make interfaces easy to use correctly, and hard to use incorrectly." At least it arguably is no worse than the alternatives previously discussed.

One acceptance barrier yet remains: I also want to apply "design by contract."

Every sample I presented was intentionally favoring constructor injection (for state dependencies) because I want to preserve "invariant" support as most commonly practiced. Namely, the invariant is established when the constructor completes.

In the sample above, the invarient is not established when object construction completes. As long as I'm doing home-grown "design by contract" I could just tell developers not to test the invariant until the Initialize(...) method is called.

But more to the point, when .net 4.0 comes out I want to use its "code contract" support for design by contract. From what I read, it will not be compatible with this last approach.

Curses!

Of course it also occurs to me that my entire philosophy is off. Perhaps I'd be told that conjuring a Foobar : IFoobar via a service locator implies that it is a service - and services only have other service dependencies, they don't have state dependencies (such as the Alpha and Omega of these examples). I'm open to listening to such philosophical matters as well, but I'd also like to know what semi-authorative reference to read that would steer me down that thought path.

So now I turn it to the community. What approach should I consider that I havn't yet? Must I really believe I've exhausted my options?

p.s. This kind of problem, along with others, prompts me to believe the whole IOC Container idea, at least in .net, is premature. It reminds me of the days when people would stand on their heads to make the "C" language feel object-oriented (adding weird macros and such). What we should be looking for is CLR and language support of IOC Containers. For example, imagine a new kind of interface called an "initiate". An initiate works like an interface, but requires a particular constructor signature as well. The rest I leave as an exercise to the student...

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

伏妖词 2024-09-05 04:52:37

您正在寻找的是“工厂适配器”或 Func

需要动态创建另一个组件的参数化实例的组件使用 Func函数。依赖类型来表示这一点:

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 容器社区之间正在开展一个名为“通用上下文适配器”的协作项目,以标准化这些和其他高阶依赖类型。该项目站点位于 http://cca.codeplex.com

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

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

http://nblumhardt.com /2010/04/介绍-autofac-2-1-rtw/
http://nblumhardt.com/2010/01/the-relationship-zoo/< /a>

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

Nick

What you're looking for is the "factory adapter" or Func<T, U>.

A component that needs to create parameterised instances of another component on the fly uses the Func<T, U> dependency type to represent this:

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, for example, will automatically provide the Func whenever IFoo is registered. It will also merge parameters into the Foo constructor (including ILogger,) and discard any that are unnecessary rather than throwing an error.

Autofac also supports custom delegates like:

delegate IFoo FooCreator(string alpha, int omega);

This way Bar can be rewritten:

class Bar
{
    FooCreator _fooCreator;

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

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

When custom delegates are used, the parameters will be matched by name, rather than by type as for Func.

Autofac has some documentation you might check out: http://code.google.com/p/autofac/wiki/DelegateFactories.

There's a collaborative project in the works between several IOC container communities called "Common Context Adapters" to standardise these and other higher-order dependency types. The project site is at http://cca.codeplex.com.

The first proposed specification from CCA covers the factory adapter, you can read it here: http://cca.codeplex.com/wikipage?title=FuncFactoryScenarios&referringTitle=Documentation.

Some other links you may find helpful:

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

I hope you'll find that while IoC containers admittedly have a long way to go before they'll be completely transparent, we're actually working quite hard to get there :)

Nick

眼泪都笑了 2024-09-05 04:52:37

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

如果这些参数因实例而异,那么我认为它们不应该作为 IoC 容器加载的对象的构造函数参数提供。这使得你的所有组件都需要查找/依赖 IoC 容器,这从一开始就违背了 IoC 容器的初衷。

要么通过公开 setter 方法使它们在对象本身上进行配置(当它们可能在对象的生命周期内发生更改时,这是适当的),或者将它们作为返回对象的工厂方法的参数(其中工厂对象在您的实例中注册) IoC 容器)。

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

If these parameters are constant for the lifetime of the application, then you can add a IConfigurationService whose sole purpose is to return these parameters to whoever needs them. The implementation of IConfigurationService might have hardcoded values, read them from a config file... whatever. Of course the implementation of IConfigurationService is retrieved through the IoC container.

If these parameters vary per instance, then I don't think they should be supplied as constructor parameters for objects loaded by your IoC container. This makes all your components need to find/depend on the IoC container, which defeats the point of an IoC container in the first place.

Either make them configurable on the object itself by exposing a setter method, (which is appropriate when they might change over the lifetime of the object) or make them parameters to a factory method that returns the object (where the factory object is registered in your IoC container).

You were hesitant to use a factory, but I think this is an elegant approach. Yeah it takes effort to create a factory, but since there is a requirement supporting the act, its not code bloat. Its a simple pattern that fulfills the requirement.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文