依赖注入和 Fluent API - 遇到一些问题
我正在编写一个流畅的接口,其使用方式如下:
xmlBuilder
.CreateFrom()
.DataSet(someDataSet) //yes I said Dataset, I'm working with a legacy code
.IgnoreSchema()
.Build
IgnoreSchema()
方法,可以是 WithSchema()
或 WithDiffGrame()
它的替代品。这些映射到 DataSet 的 WriteXml()
方法,该方法接受以下枚举:
- XmlWriteMode.WriteSchema
- XmlWriteMode.DiffGram
- XmlWriteMode.IgnoreSchema
我流畅的 API 正在调用相当于工厂对象的内容,该对象将从中创建 XML数据集。我有一个具有核心功能的抽象类型,然后是 3 个派生类型,它们反映实现 WriteXmlFromDataSet 方法的各种状态(我相信这种方法称为状态模式)。这是抽象基类:
public abstract class DataSetXmlBaseFactory : IDataSetXmlFactory
{
...
protected abstract void WriteXmlFromDataSet(XmlTextWriter xmlTextWriter);
public XmlDocument CreateXmlDocument()
{
XmlDocument document = new XmlDocument();
using (StringWriter stringWriter = new StringWriter())
{
using (XmlTextWriter xmlTextWriter = new XmlTextWriter(stringWriter))
{
WriteXmlFromDataSet(xmlTextWriter);
string content = stringWriter.ToString();
document.LoadXml(content);
return document;
}
}
}
}
这当然有效,但是当我将这段代码与依赖注入一起使用时,我在开始时提到的流畅接口中的方法遇到了麻烦。以下是这些方法的实现:
public IXmlBuild<T> WithSchema()
{
var xmlFactory = new DataSetXmlWithSchemaFactory(this.DataSet);
return GetIXmlBuild(xmlFactory);
}
public IXmlBuild<T> IgnoreSchema()
{
var xmlFactory = new DataSetXmlIgnoreSchemaFactory(this.DataSet);
return GetIXmlBuild(xmlFactory);
}
public IXmlBuild<T> WithSchemaAndDiffGram()
{
var xmlFactory = new DataSetXmlWithDiffGramFactory(this.DataSet);
return GetIXmlBuild(xmlFactory);
}
private static IXmlBuild<T> GetIXmlBuild(IDataSetXmlFactory xmlFactory)
{
string content = xmlFactory.CreateXmlDocument().InnerXml;
return new clsXmlDataSetBuild<T>(content);
}
现在我没有使用依赖注入 (DI),因为我正在新建依赖的 IDataSetXMLFactory
对象。如果我更改代码以利用 DI,该类如何知道要使用 IDataSetXmlFactory 的哪个实现?如果我正确理解 DI,则需要在调用堆栈的更高层(特别是在组合根处)做出该决定,但在那里代码不知道需要哪个确切的实现。如果我使用 DI 容器来解析(定位)上述方法中所需的实现,那么我将使用 DI 容器作为服务定位器,这被认为是反模式。
此时,将枚举传递给 IXmlDataSetFactory 实例上的 xmlFactory.CreateXmlDocument()
方法会容易得多。这当然更容易并且代码更少,但我确信状态模式和 DI 之前已经遇到过这个问题。有什么方法可以解决这个问题呢?我是 DI 新手,已开始阅读 .NET 中的依赖注入,但尚未阅读任何内容关于这个具体问题。
希望我只是遗漏了拼图中的一小部分。
更新(基于Mark Seemann的回答)
下面界面的语义模型是什么样子?示例将不胜感激。
public interface IXmlBuilder<T>
{
IXmlSourceContent<T> CreateFrom();
}
public interface IXmlSourceContent<T>
{
IXmlOptions<T> Object(T item);
IXmlOptions<T> Objects(IEnumerable<T> items);
IXmlDataSetOptions<T> DataSet(T ds);
IXmlBuild<T> InferredSchema();
}
public interface IXmlOptions<T> : IXmlBuild<T>
{
IXmlBuild<T> WithInferredSchema();
}
public interface IXmlDataSetOptions<T> : IXmlDataSetSchema<T>
{
IXmlDataSetSchema<T> IncludeTables(DataTableCollection tables);
IXmlDataSetSchema<T> IncludeTable(DataTable table);
}
public interface IXmlBuild<T>
{
XmlDocument Build();
}
public interface IXmlDataSetSchema<T>
{
IXmlBuild<T> WithSchemaAndDiffGram();
IXmlBuild<T> WithSchema();
IXmlBuild<T> IgnoreSchema();
}
除了上面提到的 IDataSetXMLFactory 之外,我还有以下扩展方法:
static class XmlDocumentExtensions
{
[Extension()]
public static void InsertSchema(XmlDocument document, XmlSchema schema)
{
...
}
}
static class XmlSchemaExtensions
{
[Extension()]
public static string ToXmlText(XmlSchema schema)
{
...
}
}
以及这些类:
public class XmlFactory<T>
{
...
public XmlFactory(IEnumerable<T> objects)
{
this.Objects = objects;
}
public XmlDocument CreateXml()
{
// serializes objects to XML
}
}
public class XmlSchemaFactory<T> : IXmlSchemaFactory<T>
{
public XmlSchema CreateXmlSchema()
{
// Uses reflection to build schema from type
}
}
I am writing a fluent interface that is used like the following:
xmlBuilder
.CreateFrom()
.DataSet(someDataSet) //yes I said Dataset, I'm working with a legacy code
.IgnoreSchema()
.Build
The IgnoreSchema()
method, could be WithSchema()
or WithDiffGrame()
in its stead. These map to the DataSet's WriteXml()
method that accepts the following enum:
- XmlWriteMode.WriteSchema
- XmlWriteMode.DiffGram
- XmlWriteMode.IgnoreSchema
My fluent API is calling what amounts to a factory object of sorts that will create the XML from the Dataset. I have an abstract type that has the core functionality and then 3 derived types that reflect the various states implementing the WriteXmlFromDataSet
method (I believe this approach is called the State pattern). Here is the abstract base class:
public abstract class DataSetXmlBaseFactory : IDataSetXmlFactory
{
...
protected abstract void WriteXmlFromDataSet(XmlTextWriter xmlTextWriter);
public XmlDocument CreateXmlDocument()
{
XmlDocument document = new XmlDocument();
using (StringWriter stringWriter = new StringWriter())
{
using (XmlTextWriter xmlTextWriter = new XmlTextWriter(stringWriter))
{
WriteXmlFromDataSet(xmlTextWriter);
string content = stringWriter.ToString();
document.LoadXml(content);
return document;
}
}
}
}
This works of course, but when I go to use this code with Dependency Injection, I run into trouble with the methods in my fluent interface mentioned at the start. The following is the implementation of those methods:
public IXmlBuild<T> WithSchema()
{
var xmlFactory = new DataSetXmlWithSchemaFactory(this.DataSet);
return GetIXmlBuild(xmlFactory);
}
public IXmlBuild<T> IgnoreSchema()
{
var xmlFactory = new DataSetXmlIgnoreSchemaFactory(this.DataSet);
return GetIXmlBuild(xmlFactory);
}
public IXmlBuild<T> WithSchemaAndDiffGram()
{
var xmlFactory = new DataSetXmlWithDiffGramFactory(this.DataSet);
return GetIXmlBuild(xmlFactory);
}
private static IXmlBuild<T> GetIXmlBuild(IDataSetXmlFactory xmlFactory)
{
string content = xmlFactory.CreateXmlDocument().InnerXml;
return new clsXmlDataSetBuild<T>(content);
}
Right now I'm not using Dependency Injection (DI) as I'm newing up the dependent IDataSetXMLFactory
objects. If I changed the code to utilize DI, how would the class know which implementation of IDataSetXmlFactory to use? If I understand DI correctly, that decision would need to be made higher up the call stack (specifically at the Composition Root), but up there the code wouldn't know which exact implementation is needed. If I used the DI container to resolve (locate) the needed implementation in the above methods, then I would be using the DI container as a Service Locator, which is considered an anti-pattern.
At this point, it would be much easier to just pass an enum to the xmlFactory.CreateXmlDocument()
method on the IXmlDataSetFactory instance. This certainly is much easier and is less code, but I'm sure this problem has been faced before with State patterns and DI. What is the way to deal with this? I'm new to DI and have started reading Dependency Injection in .NET, but have yet to read anything on this specific problem.
Hopefully, I'm just missing a small piece to the puzzle.
Update (based on Mark Seemann's answer)
What would the Semantic Model for the interface below look like? Examples would be appreciated.
public interface IXmlBuilder<T>
{
IXmlSourceContent<T> CreateFrom();
}
public interface IXmlSourceContent<T>
{
IXmlOptions<T> Object(T item);
IXmlOptions<T> Objects(IEnumerable<T> items);
IXmlDataSetOptions<T> DataSet(T ds);
IXmlBuild<T> InferredSchema();
}
public interface IXmlOptions<T> : IXmlBuild<T>
{
IXmlBuild<T> WithInferredSchema();
}
public interface IXmlDataSetOptions<T> : IXmlDataSetSchema<T>
{
IXmlDataSetSchema<T> IncludeTables(DataTableCollection tables);
IXmlDataSetSchema<T> IncludeTable(DataTable table);
}
public interface IXmlBuild<T>
{
XmlDocument Build();
}
public interface IXmlDataSetSchema<T>
{
IXmlBuild<T> WithSchemaAndDiffGram();
IXmlBuild<T> WithSchema();
IXmlBuild<T> IgnoreSchema();
}
In addition to IDataSetXMLFactory mentioned above, I also have the following extension methods:
static class XmlDocumentExtensions
{
[Extension()]
public static void InsertSchema(XmlDocument document, XmlSchema schema)
{
...
}
}
static class XmlSchemaExtensions
{
[Extension()]
public static string ToXmlText(XmlSchema schema)
{
...
}
}
and these classes:
public class XmlFactory<T>
{
...
public XmlFactory(IEnumerable<T> objects)
{
this.Objects = objects;
}
public XmlDocument CreateXml()
{
// serializes objects to XML
}
}
public class XmlSchemaFactory<T> : IXmlSchemaFactory<T>
{
public XmlSchema CreateXmlSchema()
{
// Uses reflection to build schema from type
}
}
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(2)
在我看来,您正在发现根据 API 所针对的对象模型定义 Fluent API 的局限性。正如 Jeremy Miller 指出的那样,让 Fluent API 构建语义模型通常更好< /a> 然后可以用来构造所需的对象图。
这是我分享的经验,我发现它有助于弥合 Fluent API 和 DI 之间的明显差距。
基于最初提出的 Fluent API,语义模型可能像这样简单:
It seems to me that you are discovering the limits of defining a Fluent API in terms of the object model targeted by the API. As Jeremy Miller points out, it's often better to let the Fluent API build a Semantic Model which can then be used to construct the desired object graph.
This is an experience I share, and which I find helps bridge the apparent gap between Fluent APIs and DI.
Based on the original Fluent API presented, the Semantic Model might be something as simple as this:
您的设计可以与 DI 配合使用。这里的技巧是注入工厂,而不是组件本身。您的分析完全正确,即应由组合根来决定
DataSetXmlFactory
的实现,但是您第一步就错了。流畅的构建器完全有权根据实际需要请求不同的实现。所有这些工厂都确认相同的接口这一事实更多的是它们操作的副作用,而不是它们的身份的一部分。将此视为 XML 工厂具有创建文档的能力,而不是创建文档的 IS-A[n] 实体。
IoC 容器辅助系统(主要是构造函数注入风格)不能很好地处理接收注册时未定义的构造函数参数的组件。这意味着某些框架甚至不允许您执行此类解析:
var myClass = container.Resolve(additionalConstructorArgument);
。这意味着为我们添加另一个间接级别(即工厂)。下面是我会采用的设计。这种特殊的设计有点尴尬,简单的实现会看到像 DataSetXmlWithSchemaFactoryFactory 这样的类。因此,我要做的第一件事是将
DataSetXmlWithSchemaFactory
重命名为DataSetXmlBuilderBase
。恕我直言,这些类所做的事情更接近构建器模式,而不是抽象工厂模式。我还将介绍 Fluent 构建器将使用的一组构建器工厂接口。关于标记接口的注释。这些接口实际上并没有向它们继承的接口添加任何新内容,但它们在为该组件注册意图时很有用。因此,当我们请求
IDataSetWithSchemaBuilderFactory
时,我们要求容器为我们提供一个构建器工厂,用于创建带有架构的 XML 文档。因为它是一个接口,所以我们可以将其替换为容器级别的另一个工厂,而无需接触 FluentXmlBuilder。您可以观看 Udi Dahan 关于明确角色的精彩演讲有关具有明确意图的编程风格的更多背景信息。Your design can be made to work with DI. The trick here is to inject factories, not components themselves. You are absolutely correct in your analysis that the decision about which implementation of
DataSetXmlFactory
should be made by composition root, however you got the first step wrong.The fluent builder is well within its rights to request different implementations based on what it actually needs. The fact that all of those factories confirm to the same interface is more of a side effect of their operation, not part of their identity. Think of this as XML factory HAS-A capability to create a document, not IS-A[n] entity that creates documents.
IoC container aided systems(mainly of constructor injection flavour) do not deal well with components receiving constructor arguments that are not defined at registration. This means that some frameworks wouldn't even let you do this kind of resolution:
var myClass = container.Resolve<MyClass>(additionalConstructorArgument);
. This means adding another level of indirection - i.e. a factory - to this for us.Below is a design I would go with. This particular design is a bit awkward and a naive implementation will see classes like
DataSetXmlWithSchemaFactoryFactory
. So the first thing I'm going to do is to renameDataSetXmlWithSchemaFactory
toDataSetXmlBuilderBase
. IMHO what those classes do is closer to builder pattern than to abstract factory pattern. I will also introduce a set of builder factory interfaces that the fluent builder will use.A note on marker interfaces. These interfaces don't actually add anything new to the interface they inherit, but they are useful in registering intent for that component. So when we ask for
IDataSetWithSchemaBuilderFactory
, we are asking the container to give us a builder factory that creates XML document with schema. Because it's an interface, we can swap it out for another factory at the container level without touchingFluentXmlBuilder
. You can watch an excellent presentation by Udi Dahan on making roles explicit for some more background on the programming style with explicit intent.