Wednesday, November 12, 2008

Method Chaining to create your DSL

I just attended a London DNUG hosted by Ian Cooper regarding Internal DSLs in C#3.0. It was a very interesting session where Ian covered some very similar content to JP Boodhoo's series of 5 "Demystifying patterns" on DNRTV.com. Firstly Ian Covered simply what a DSL is and how there can be DSL internal to .NET. His examples were RhinoMock (and nMock), LINQ and implementations of a fluent API using strategy patterns and method chaining.

While I have seen people use method chaining before primarily in demos or when I have been using RhinoMocks but I have never heard any guidance on how to construct them. As a quick intro to method chaining and why they constitute a DSL here is an example similar to Ian's.

In this example we create a Meeting type using a MeetingBuilder.

var meeting = new MeetingBuilder()
    .On("C# Internal DSLs")
    .By("Ian Cooper")
    .And("Someone else")
    .At( new Address()
        {
            Street1 = "Skills Matter"
            Street2 = "1 something lane"
            PostCode = "SE1ABC"
        })
    .From(new DateTime(2008, 11, 11, 18, 30))
    .Until(new DateTime(2008, 11, 11, 20, 00))
    .Create();
The general Idea here is that we create something that looks more like English language than standard C# code.

So having shown a simple example of Method chaining, I think it is fair to say that it is quite readable and that non technical people (eg Mum) could probably figure out what I am doing here

Ian Copper and somebody else is presenting "C# Internal DSLs" at Skills Matter on 18:30 11th Nov.

If we analyse the code, first thing I would like to mention is that Ian recommends that all methods from a chaining builder (in this case MeetingBuilder) should return this except the Create() method that returns the final object. Now having considered this, that means that we can deduce that MeetingBuilder has a method On(string). Now considering that On(string) is a method on a Builder it should also return the Builder. Cool. By that rational we can also deduce that By(string), And(string), At(Address), From(DateTime), To(DateTime) and Create() are all methods on MeetingBuilder.

Well that is not so interesting until we consider that if we want to create a really fluent API. For the API to be fluent it should not only be readable, we should provide guidance for consuming it too. Referring back to the example we can see the we probably have mandatory method calls and optional methods. In this case only the And(string) method is optional, all of the others should be mandatory. So how can we drive the consumer/developer to know how to use the API if all methods return this?
The answer : Interfaces.

By setting the return types of the methods to interfaces, we can constrain what the next method can do. We still return this, which then tells us we need to implement a bunch of interfaces. The first method this example requires is On(string) so we need an interface like:

interface IMeetingSubjectBuilder
{
    object On(string);
}

but we cant just return object, we need to return an interface that guides the developer to provide the name of the presenter so we expand our interface definitions

interface IMeetingSubjectBuilder
{
   IMeetingPresenterBuilder On(string subject);
}
interface IMeetingPresenterBuilder
{
    IMeetingPresenterOrAddressBuilder By(string presenter);
}
interface IMeetingPresenterOrAddressBuilder 
{
    IMeetingPresenterOrAddressBuilder And(string otherPresenter);
    IMeetingFromTimeBuilder At(Address address);
}

In the above step we have actually done a few things. Firstly we have swapped out the rubbish return type of object for the IMeetingPresenterBuilder. This will then guide our developers to use the methods on that interface. Next we declare that new interface has the By(string) method. This method returns a third interface. The third interface provides two methods. The first method is the And(string) method that allows us to provide additional presenters. Note that its return type is the interface that defines it, which allows a us to recursively call it to add multiple presenters. The second method provides a way to continue providing Address data and eventually other data.

To jump ahead quickly, here is what the final set of interfaces might look like

interface IMeetingSubjectBuilder
{
   IMeetingPresenterBuilder On(string subject);
}
interface IMeetingPresenterBuilder
{
    IMeetingPresenterOrAddressBuilder By(string presenter);
}
interface IMeetingPresenterOrAddressBuilder 
{
    IMeetingPresenterOrAddressBuilder And(string otherPresenter);
    IMeetingFromTimeBuilder At(Address address);
}
interface IMeetingFromTimeBuilder
{
    IMeetingToTimeBuilder From(DateTime startTime);
}
interface IMeetingToTimeBuilder
{
    IMeetingCreator Until(DateTime endTime);
}
interface IMeetingCreator
{
    Meeting Create();
}

Well this is good progress, so all we have to do now is declare a class that implements these interfaces.

class MeetingBuilder : IMeetingSubjectBuilder, IMeetingPresenterBuilder, IMeetingPresenterOrAddressBuilder, IMeetingFromTimeBuilder, IMeetingToTimeBuilder, IMeetingCreator
{
    public IMeetingPresenterBuilder On(string subject){...}
    public IMeetingPresenterOrAddressBuilder By(string presenter){...}
    public IMeetingPresenterOrAddressBuilder And(string otherPresenter){...}
    public IMeetingFromTimeBuilder At(Address address){...}
    public IMeetingToTimeBuilder From(DateTime startTime){...}
    public IMeetingCreator Until(DateTime endTime){...}
    public Meeting Create() {...}
}

One final problem here is that if we were to use this as per the first example we would find that we could call any of the methods straight from the constructor which could confuse the developer. To better guide the developer we can simply implement all of the interface explicitly except for IMeetingSubjectBuilder.

class MeetingBuilder : IMeetingSubjectBuilder, 
    IMeetingPresenterBuilder, 
    IMeetingPresenterOrAddressBuilder, 
    IMeetingFromTimeBuilder, 
    IMeetingToTimeBuilder, 
    IMeetingCreator
{
    public IMeetingPresenterBuilder On(string subject){...}
    IMeetingPresenterOrAddressBuilder IMeetingPresenterBuilder.By(string presenter){...}
    IMeetingPresenterOrAddressBuilder IMeetingPresenterOrAddressBuilder.And(string otherPresenter){...}
    IMeetingFromTimeBuilder IMeetingPresenterOrAddressBuilder.At(Address address){...}
    IMeetingToTimeBuilder IMeetingFromTimeBuilder.From(DateTime startTime){...}
    IMeetingCreator IMeetingToTimeBuilder.Until(DateTime endTime){...}
    Meeting IMeetingCreator.Create(){...}
}

Now when we create an instance of MeetingBuilder we are guided as to which methods are valid in which order and have a nice fluent API. From the constructor we have the On(string) method available, then from there we have the By(string) and so on.

Creating a Fluent API (and therefore an internal DSL) does require more work and some extra effort at design time but it does make for very nice coding experience for the end developer. For more information see what the godfather has to say here : http://martinfowler.com/dslwip/MethodChaining.html

1 comment:

Lee Campbell said...

Skills matter have uploaded the actual video of Ian's presentation here