2

I'm trying to modify some existing code to use Castle Windsor as an IoC container.

The application in question is document-oriented. So it has an object graph that relies on data for specifying the data source that can't be known at registration time, and will change every time the object graph is resolved. However, this dependency is a few layers into the object graph, so I can't just pass it as an argument to container.Resolve() since Windsor doesn't propagate inline dependencies.

The solution I've come up with instead is to use the typed factory facility to generate a factory which can resolve every dependency in the graph, and have Document's constructor take an instance of this factory as well as a string specifying the data source. The factory is specified at registration time, and the data source is specified at resolution time. The constructor takes over from there, manually polling the factory to resolve is dependencies. The result looks something like this:

public interface IDataSource { /* . . . */ }
public interface IWidgetRepository { /* . . . */ }
public interface IWhatsitRepository { /* . . . */ }

// Implementation generated by Windsor's typed factory facility
public interface IFactory
{
    IDataSource CreateDataSource(string path);
    IWidgetRepository CreateWidgetRepository(IDataSource dataSource);
    IWhatsitRepository CreateWhatsitRepository(IDataSource dataSource);

    void Release(IDataSource dataSource);
    void Release(IWidgetRepository widgetRepository);
    void Release(IWhatsitRepository whatsitRepository);
}

public class Document
{
    private readonly IDataSource _dataSource;
    private readonly IWidgetRepository _widgetRepository;
    private readonly IWhatsitRepository _whatsitRepository;

    public Document (IFactory factory, string dataSourcePath)
    {
        _dataSource = factory.CreateDataSource(dataSourcePath);
        _widgetRepository = factory.CreateWidgetRepository(_dataSource);
        _whatsitRepository = factory.CreateWhatsitRepository(_dataSource);
    }
}

This absolutely works, and does accomplish the goal of having Windsor take charge of dependency resolution. Or at least, I can easily reconfigure the application by modifying the registration code. At the moment I'm still making one call to container.Resolve() for every Document that gets created, but I believe that sin can easily be amended with a second Typed Factory.

However, it still feels wrong. Windsor is in charge of newing up objects and (somewhat) managing their lifetimes, sure. But it's not really injecting those dependencies into Document's constructor; instead the constructor is pulling them out of the factory. Worse, by passing the instance of IDataSource into the factory methods, it's taken charge of managing the object graph. That seems like a huge subversion of the inversion to me.

So, what am I missing?

EDIT

To clarify, what I think I'm supposed to be shooting for is for Document's constructor to look more like the below.

public Document (IDataSource dataSource, IWidgetRepository widgetRepository, IWhatsitRepository whatsitRepository)
{
    _dataSource = dataSource;
    _widgetRepository = widgetRepository;
    _whatsitRepository = whatsitRepository;
}

That way Windsor is in direct control of supplying all the objects' dependencies using constructor injection. That's actually what the constructor signature looks like in the original code, but I was unable to get it to work with Windsor because container.Resolve() doesn't propagate inline dependency parameters. So I can't just do:

var document = container.Resolve<IDocument>(new { dataSourcePath = somePath });  // throws an exception

because Windsor doesn't pass dataSourcePath on to DataSource's constructor, much less make sure that a DataSource instantiated with that path is passed on to the other constructors.

An answer to another question points out that this is by design - doing otherwise would introduce coupling. This is a no-no, since one shouldn't mandate or assume that implementations of an interface have particular dependencies. Sadly, that points out another way I think the code I came up with is wrong, since Factory.CreateWidgetRepository() and Factory.CreateWhatsitRepository() imply just that sort of assumption.

4

2 回答 2

9

我相信我找到了正确的解决方案。显然,可用的文档不是很明确(冗长?),不足以在我前十二次阅读这个概念时通过我厚厚的头骨猛击这个概念,所以为了其他人,我将尝试在这里更详细地记录它可能和我一样无助。(在接受之前我也会让它坐一会儿,希望其他人可能会提出任何其他/更好的建议。)

长话短说,Typed Factory Facility 不适合这项工作。

--

诀窍是在流畅的注册 API 中使用 DynamicParameters 工具,这在此处和此处的最后一节中(相当稀疏地)记录了。

DynamicParameters 允许您直接修改在要求容器解析组件时提供的内联参数集合。然后,该字典可以向上传递到解析管道,使其可用于子依赖项。

DynamicParameters有三个重载,每个重载都以一个委托作为其参数。这些委托类型没有明确记录,所以为了后代,这里是 ReSharper(我懒得下载源代码。)告诉我他们的声明看起来像:

public delegate void DynamicParametersDelegate(IKernel kernel, IDictionary parameters);
public delegate ComponentReleasingDelegate DynamicParametersResolveDelegate(IKernel kernel, IDictionary parameters);
public delegate ComponentReleasingDelegate DynamicParametersWithContextResolveDelegate(IKernel kernel, CreationContext creationContext, IDictionary parameters);

DynamicParametersDelegate对于最基本的情况,您只需要提供不会由容器管理的参数。这可能对我有用,但为了与我首先找到复杂选项的倾向保持一致,我最终为第二个选项做了一条直线,即提供一个手动从容器中提取动态参数的委托。由于 Windsor 在这种情况下管理组件的生命周期,因此您需要确保它已被发布。这就是DynamicParametersResolveDelegate出现的地方 - 它就像第一个一样,除了它还返回一个ComponentReleasingDelegate( public delegate void ComponentReleasingDelegate(IKernel kernel);) ,Windsor 可以使用它在适当的时间发布组件。

(第三个,DynamicParametersWithContextResolveDelegate大概是为了修改创建上下文。我对 Windsor 的工作原理了解得不够多,无法真正理解前一句的意思,所以我只好就此打住了。)

这使我可以将示例中的构造函数替换为更好看的构造函数:

public class Document
{
    private readonly IDataSource _dataSource;
    private readonly IWidgetRepository _widgetRepository;
    private readonly IWhatsitRepository _whatsitRepository;

    public Document (IDataSource dataSource, IWidgetRepository widgetRepository, IWhatsitRepository whatsitRepository)
    {
        _dataSource = dataSource;
        _widgetRepository = widgetRepository;
        _whatsitRepository = whatsitRepository;
    }
}

工厂被完全移除。相反,神奇的是组件注册代码IDocument

container.Register(Component.For<IDocument>()
                   .ImplementedBy<Document>()
                   .DynamicParameters( 
                       (k, d) =>
                       {
                           // ask for an IDataSource, passing along any inline
                           // parameters that were supplied in the request for 
                           // an IDocument
                           var ds = k.Resolve<IDataSource>(d);

                           // Add it to the dictionary.  This makes it available
                           // for use when resolving other dependencies in the tree.
                           d.Add("DataSource", ds);

                           // Finally, pass back a delegate which can be used to release it
                           return (r) => r.ReleaseComponent(ds);
                       }));

所以现在我可以要求一个IDocument与我一直在寻找的代码行完全相同的代码:

var document = container.Resolve<IDocument>(new { dataSourcePath = somePath });

并且容器首先调用该DynamicParameters委托,该委托为容器提供DataSource所提供路径的 a 。从那里容器能够自己解决其余的问题,以便将相同的实例DataSource传递给依赖关系图中所有其他三个对象的构造函数。

于 2012-12-11T17:40:17.063 回答
0

我有同样的问题。我对上述解决方案的问题是您将致电:

var document = container.Resolve<IDocument>(new { dataSourcePath = somePath });

在每个需要 IDocument 的地方。我认为我真的应该在程序中的一个点解析我的所有对象,而不是在其他任何地方使用容器。换句话说,从容器中解析一次工厂,然后在程序的其他点使用该工厂。我认为你最初是在正确的轨道上。说了这么多,我无法提供更好的解决方案。

于 2017-03-22T15:06:35.397 回答