On Fluent Interfaces and Friction

So I decided to start that NHibernate Mapping DSL project andopen-source it.  Right now it doesn't do anything, but you're free totake a look at http://www.assembla.com/space/nhibernate-mapping

I wrote all of this code test-first, and it allowed me to tackle theinterface and flow the way I thought might be useful.  I ended up withsomething that was mildly usable like this:

IMapping mapping = Mapping.For<Product>()
.Identity("Id")
.Property("Name")
.Property("Price")
.Map();

So this allows me to quickly accept the defaults of the mappings, so NHibernate will assume that I have ...
  • the column names are the same as the property names
  • the type of the column matches the type of the property and it can be picked up via reflection
  • the identity generator of the primary key is Identity
  • the identity/properties have setters
  • the "Name" and "Price" columns are nullable
This isn't always true of course, so we need to be able to specify where we deviate from the defaults.  So how can we accomplish this using a fluent interface?  We could resort to adding more overloads to the Identity() method, so you might have....

IMapping mapping = Mapping.For<Product>()
.Identity("Id", "product_id", Access.NoSetterCamelcaseUnderscore, Generator.Native)
.Property("Name", "product_name", false)
.Property("Price", false)
.Map();
But we don't know how many settings the user might want to specify.  If we have 4 simple, independent settings, then we'd have to support 4! method overload options!  Yuck!

public IPropertySpecificationPredicate Property(string name);
public IPropertySpecificationPredicate Property(string name, string columnName);
public IPropertySpecificationPredicate Property(string name, string columnName, DbType type);
public IPropertySpecificationPredicate Property(string name, string columnName, DbType type, int length);
public IPropertySpecificationPredicate Property(string name, string columnName, bool nullable);
...
This is just the beginning.  What if you wanted to only specify the property name and the nullability?  We quickly end up in an anti-pattern I like to call overload explosion

We can do a bit better, right?  I started to gather my thoughts and came up with this syntax:

IMapping mapping = Mapping.For<Product>()
.Identity("Id")
.Column("product_id")
.Generator(Generator.Native)
.Map();
So now the Identity() method takes a single parameter, the name of the property.  Anything else is optional.  The return type is now something like an IIdentityBuilder interface, which accepts methods on altering the identity object that its building.  Here you can see that we only specified the settings that we wanted, nothing else.  We still had to have a way to "pop" the current object (and thus the entire mapping) so that we can indicate to the builder that we are done building the property and you can return the IMapping object next.  This feels awkward for now, but for now it's ok.

Let's extend this pattern.  Once I'm working with the IMappingBuilder interface, I can now see methods called Generator(), Access(), Column(), etc.  There's nothing there to prevent me from calling .Column("...").Column("...").Column("...").Column("...") which I guess isn't such a big deal.  All it's doing is setting the column property of this object we're building.

Now I'm noticing that there has to be a spot where the user gets finished with the mapping identity and now has some choices.  The obvious next operation would be to create a property, but how do I pop the current context and start working on a new one?

Maybe something like this?

IMapping mapping = Mapping.For<Product>().Identity("Id").Column("product_id").Generator(Generator.Native).AndProperty("Name").Nullable(false).Map();

I'm already started to notice that my identity and property elements are getting lost in the noise.  If I keep up this pattern not only do I have TONS of ISomethingPredicateBuilder objects, I also remember that it's not only properties that I can add at any time.  I could add a Bag, a Set, a List, or any number of other mapping elements.

Another route I might take is using anonymous to provide me with the flexibility of code within the interface.  Take a look:

IMapping mapping = Mapping.For<Product>().Identity("Id", delegate(Identity i) { i.column = "product_id"; i.generator = Generator.Native;}).AndProperty("Name", delegate(Property p) { p.column = "product_name"; p.nullable = false;}).Map();
Here I have a lot more flexibility over the different ways that I define various mapping elements.  The only part of this that is really cumbersome is the delegate syntax itself.

This is probably where Boo or Ruby would come in and save the day because of it's super-dynamicness-flexibility-extraordinaire, but I'd like to see how far I can push C# for now.  At least until C# 3.0 comes out, where I'll get object initializers, lamba expressions, and a few other neat tricks that really make this stuff more fun to sculpt.

What about you?  What types of syntax do you find most readable, usable?
#1 Dario Quintana avatar
Dario Quintana
11.09.2007
7:55 PM

Hi Ben, good work.I like more this sintaxis:IMapping mapping = Mapping.For<Product>() .Identity("Id", "product_id", Access.NoSetterCamelcaseUnderscore, Generator.Native) .Property("Name", "product_name", false) .Property("Price", false) .Map();I think that the method .Map() could be removed without complication, besides, I think there is a little posibility to forget it when write the code.The dsl using delegates... mmm... more complicated to read, I prefer the xml :)Best regards


#2 Joe Ocampo avatar
Joe Ocampo
11.09.2007
8:28 PM

How about this?By using the "using" block you don't have to explicitly call Map.IMapping mapping;using (mapping = Mapping.For<Product>()){mapping.IdentifiedBy("Id");mapping.Property("Name").Length(100);mapping.Property("Description").Length(5000);mapping.Property("Price").Type(typeof(decimal));}


#3 Joe Ocampo avatar
Joe Ocampo
11.09.2007
8:31 PM

I almost forgot, I agree with Dario about the delegate.Speaking from experience the average developer has a hard time comprehending delegates.The noise that they create interferes with solubility.I know the example I gave isn't much of a fluent interface but it reads well to me.BTW Great job!


#4 Ben Scheirman avatar
Ben Scheirman
11.09.2007
8:37 PM

I see what you mean about the delegates, but I like the fact that I get a first class object that I can set properties on, and within context.C# 3.0 will allow me to push this pattern a bit farther I think.I'm going to tinker with the using syntax that you put there.It looks pretty friendly.


#5 Dario Quintana avatar
Dario Quintana
11.09.2007
10:16 PM

It's really neccesary the .Map() ? That's remember me that Static class with just one method:MakeSomething.Do(); Maybe with Map(), will be more easy to implement the DSL, without it, will look better, but all method will must return IMapping.About the using in replace for Map(), I think it is fashionable in this days and the code looks pretty. But, with a project with 100 mappings (and...I'm being generous) I don't know, could be more code innecesary.Regards