3

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
    }
}
Matt
  • 14,353
  • 5
  • 53
  • 65

2 Answers2

6

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:

public class MySemanticModel
{
    public DataSet DataSet { get; set; }
    public bool IgnoreSchema { get; set; }
    // etc...
}
Mark Seemann
  • 225,310
  • 48
  • 427
  • 736
  • thanks for coming over here to add your input. I've read the article, but I'm still struggling a bit as this is my first real attempt at both an internal DSL and DI. In my example, after more thought, couldn't I just inject (via the constructor) three dependencies of type IDataSetXMLFactory (one for each of the three categories)? I think this would solve the issue would it not? As regards the semantic model, I definitely see how that can be helpful, but I don't see how it helps here specifically. – Matt May 30 '11 at 19:05
  • The problem is that you are trying to build the expression and make it executable at the same time (as witnessed by the WithSchemaAndDiffGram method). Just add a few more options and you'll have too many permutations to be manageable. It would be better to first use the Fluent API to build up the Semantic Model. Once you have that, you can basically inject it into your real object model and feed it from that. You don't want to use DI in your Semantic Model as it's basically just a graph of data that you can subsequently feed to your object model (which *can* use DI in a meaningful way). – Mark Seemann May 30 '11 at 20:44
  • I've updated my question to include the interface of the Fluent API that defines how it would be used (this is the easy part). I also have the domain and helper objects defined. So I guess it's the the Semantic Model that I'm struggling with. An example of a semantic model using my code sure would be helpful. – Matt May 30 '11 at 22:35
  • Thanks again for taking the time to help. So instead of calling directly to IDataSetXMLFactory in the Fluent API while building the expression, I should use the Semantic Model to store my data? Then when I want to build everything at the end I inject the semantic model into the domain objects (IDataSetXMLFactory classes in this case)? – Matt May 31 '11 at 12:53
  • That sounds about right. The Build() method is what triggers the transition from semantic model to the underlying object model. – Mark Seemann May 31 '11 at 16:18
1

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 rename DataSetXmlWithSchemaFactory to DataSetXmlBuilderBase. 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.

public abstract class DataSetXmlBuilderBase : IDataSetXmlBuilder
{
  //existing implementation
}


public interface IDataSetXmlBuilderFactory
{
   IDataSetXmlBuilder Create(DataSet dataset);
}

//Marker interfaces for different builder facotries
public interface IDataSetWithSchemaBuilderFactory : IDataSetXmlBuilderFactory
{
}
public interface IDataSetXmlIgnoreSchemaBuilderFactory : IDataSetXmlBuilderFactory
{
}
public interface IDataSetXmlWithDiffGramBuilderFactory : IDataSetXmlBuilderFactory
{
}

//factory implementation
public class DataSetWithSchemaBuilderFactory : IDataSetWithSchemaBuilderFactory 
{
   public IDataSetXmlBuilder Create(DataSet dataset)
   {
      return new DataSetWithSchemaBuilder(dataset);
   }
}

//Our fluent builder now receives multiple factories in its constructor and can perform its task without referencing the IoC container

public class FluentXmlbuilder
{
    readonly IDataSetWithSchemaBuilderFactory _withSchemaBuilderFactory;
    readonly IDataSetXmlIgnoreSchemaBuilderFactory _ignoreSchemaBuilderFactory;
    readonly IDataSetXmlWithDiffGramBuilderFactory _withDiffGramBuilderFactory;

    public FluentXmlbuilder(IDataSetWithSchemaBuilderFactory withSchemaBuilderFactory,  IDataSetXmlIgnoreSchemaBuilderFactory ignoreSchemaBuilderFactory,IDataSetXmlWithDiffGramBuilderFactory withDiffGramBuilderFactory)
    {
       _withSchemaBuilderFactory = withSchemaBuilderFactory;
       _ignoreSchemaBuilderFactory = ignoreSchemaBuilderFactory;
       _withDiffGramBuilderFactory = withDiffGramBuilderFactory;
    }
    public IXmlBuild<T> WithSchema()
    {
        var xmlFactory = _withSchemaBuilderFactory.Create(this.DataSet);
        return GetIXmlBuild(xmlFactory);
    }
    public IXmlBuild<T> IgnoreSchema()
    {
        var xmlFactory = _ignoreSchemaBuilderFactory.Create(this.DataSet);
        return GetIXmlBuild(xmlFactory);
    }
    public IXmlBuild<T> WithSchemaAndDiffGram()
    {
        var xmlFactory = _withDiffGramBuilderFactory.Create(this.DataSet);
        return GetIXmlBuild(xmlFactory);
    }
    private static IXmlBuild<T> GetIXmlBuild(IDataSetXmlFactory xmlFactory)
    {
        string content = xmlFactory.CreateXmlDocument().InnerXml;
        return new clsXmlDataSetBuild<T>(content);
    }

}

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 touching FluentXmlBuilder. You can watch an excellent presentation by Udi Dahan on making roles explicit for some more background on the programming style with explicit intent.

Igor Zevaka
  • 74,528
  • 26
  • 112
  • 128