A place to discuss Development techniques, .NET, XNA, NHibernate or anything else that tickles your fancy

Thursday, March 26, 2009

Fluent Method Chaining

So, something worth noting is the concept of Method Chaining. What I mean here, is the ability to call a method on an object, have that method return us an object, which we then use to perform actions on, which returns us an object which we then use to perform actions on, which return...oh you get the idea.

In general, I find this method exceptionally useful if you're creating a suite of objects that operate together to set up a configuration. A good example of this might be from the Fluent NHibernate mapping API. First, lets check out how to do a subclass mapping without using fluent chaining:



   1:  DiscriminatorPart<string, Document> subClassDiscriminator = DiscriminateSubClassesOnColumn<string>("DiscriminatorValue");

   2:  SubClassPart<string, Document, Manager> subClassPart = null;

   3:  DiscriminatorPart<string, Document> discriminatorPart = subClassDiscriminator.SubClass<Manager>("DocumentManager", x => {subClassPart=x;} );

   4:  ManyToManyPart<Document> manyToManyPart = subClassPart.HasManyToMany(x => x.Documents);

   5:  manyToManyPart.AsMap("indexColumn");

   6:  manyToManyPart.Inverse();

   7:  manyToManyPart.Cascade.AllDeleteOrphan();

   8:  manyToManyPart.LazyLoad();

   9:  manyToManyPart.WithForeignKeyConstraintName("FK_Managers_Documents");



Not too bad in all actuality. We've all written code like the one above. But what if there was a different way to get the same thing? Lets look at the same functional code, but this time with Fluent NHibernate's chaining:


   1:  DiscriminateSubClassesOnColumn<string>("DiscriminatorValue")

   2:                  .SubClass<DocumentManager>("DocumentManager", mgr =>

   3:  {

   4:  mgr.HasManyToMany(x => x.Documents)

   5:    .AsMap("indexColumn")

   6:        .Inverse()

   7:        .Cascade.AllDeleteOrphan()

   8:        .LazyLoad()

   9:        .WithForeignKeyConstraintName("FK_Managers_Documents");

  10:     });


Let me explain the basics of what's going on here:
1: We're calling a method on our class, which returns to us a DiscriminatorPart object.
2: That DiscriminatorPart has a SubClass() method on it, which returns to us the same DiscriminatorPart that was created on line 1
4: The subclass DocumentManager is being mapped in the lambda. This is chained to call a HasManyToMany method on the DocumentManager, returning us a ManyToManyPart
5: The ManyToManyPart has a Map() method called, setting up the collection type, and returning us itself as a ManyToManyPart
6: The ManyToManyPart has an Inverse() method on it which sets a flag telling the mapping to write an inverse="true" attribute, and then returns us itself as a ManyToManyPart
7: The ManyToManyPart has a Cascade() attribute set on it which creates a CollectionCascadeExpression on the ManyToManyPart. The chain from the CollectionCascadeExpression sets an attribute for the type of cascade, in this case, AllDeleteOrphan. The AllDeleteOrphan returns us the original ManyToManyPart object that Cascade was called on
8-9: Other attributes are set via chained methods returning the ManyToManyPart

What's the difference? Hopefully it's obvious. But I'll list out the main points I can see:
- Less verbose code. Simply put, the largest benefit is that you have to write less. There's less repetition on the screen regarding accessing the same object over and over
- Reads better. Because there's less code on the screen, it's easier to focus on just the parts that convey meaning, and read intent. When debugging or reviewing code, gathering intent is most likely the highest investment you have in the code.
- More meaningful interface. We are able to specify what is allowed to be done at what point, and control the flow of access. In effect, we can guide the subscriber to our API down the "happy path".

Regarding that last point, I would highly recommend the return types to fluently chainable methods to be exclusively interfaces. This allows you to cut down on the noise and dead end paths that might appear when you return an implemented class. Even Fluent NHibernate is guilty of violating this on many occasions, so don't feel too bad if you're tossing around concretes.

How to tell when to use Fluent Method Chaining? The end result should be a much more elegant way in which to interact with your model. If you don't immediately notice the improvement, or it feels like you're having to do more work than you used to when interacting with dependant operations on your class, then go back and re-evaluate your usage of this technique. Perhaps you're using too large of an interface as the return type. Perhaps you're forcing chaining in situations where it doesn't make much logical sense to.

Not every situation needs this approach. With enough experience, you'll quickly find which scenario's work best and which are better left to traditional declarations.

0 comments:

Post a Comment