Quantcast
Channel: Jorge Rowies » Fluent API
Viewing all articles
Browse latest Browse all 2

How to build a basic fluent interface in 8 steps

0
0

Disclaimer: the aim of this post is not to show a comprehensive list of the different techniques that can be used to write a fluent API, tons of good information is out there on the web and. If you are very interested in the subject, please take a look at Martin Fowler’s Domain-Specific Languages.

Now that I took that load off my back, let’s start writing a fluent interface.

Imagine we are developing an API to publish blog posts to multiple blogging platforms (Blogger, WordPress, TypePad and so on…), chances are that our (super-simplified) domain model will be something similar to this:

So, creating a new blog post should be:

var post = new BlogPost();
post.Title = "How to build a basic fluent interface in 8 steps";
post.Body = "<html>...</html>";

var author = new Author(); // We should check the authors repository
                           // before creating a new one
author.Name = "John Doe";
author.Email = "johndoe@email.com";
author.Twitter = "@johndoe";
post.Author = author;

post.Tags = "Fluent API, Internal DSL";

Our domain model is very simple and the code for creating a blog post is not so bad to read or write, so in this case implementing a fluent interface might be somewhat unnecessary; but… it’s perfect for us to learn how to write a fluent API. Shall we?

Step 1 – Writing a draft of the interface

Write (in notepad) a draft of the fluent interface showing how you want it to look like (don’t think too much now on how you will implement the interface, just play around with the ideas)

This is what I came up with:


var post = Post
.Title("How to build a basic fluent interface in 8 steps")
.Body("...")
.Author()
.Name("John Doe")
.Email("johndoe@email.com")
.Twitter("@johndoe")
.Tags("Fluent API, Internal DSL");

Step 2 – Using a test-first approach

Once you found an option that satisfies you, put it into a test in Visual Studio and add the code you will need to validate the outcome of the fluent interface. Depending on the size of the API, you might want to do this gradually. In this case, we are going to leave the “Author” part out of the scope for now.

In the code below we are creating two BlogPost instances, the first one using the standard API and the second one using our new fluent API. After both instances are created, they are passed to the CheckEquivalence method. This method should check if both instances of BlogPost have the same title, body and tags.

[TestCase]
public void ConfiguringPostWithoutAuthor()
{
    var expected = new BlogPost();
    expected.Title = "How to build a basic fluent interface in 8 steps";
    expected.Body = "<html>...</html>";
    expected.Tags = "Fluent API, Internal DSL";

    var post = Post
        .Title("How to build a basic fluent interface in 8 steps")
        .Body("<html>...</html>")
        .Tags("Fluent API, Internal DSL");
    BlogPost actual = post.Build();

    Assert.IsTrue(TestsHelper.CheckEquivalence(expected, actual));
}

This test, of course, won’t even compile.

Step 3 – Starting to implement the interface

Now, let’s start implementing the Post class. Note that the Title method is static so we can call it without creating a new instance of Post.

public class Post
{
    public static Post Title(string title)
    {
        var post = new Post();
        post.TitleValue = title;
        return post;
    }
}

Step 4 – Adding more methods to the fluent interface

Once we have our starting point, continue adding the remaining methods (this time as instance methods, not static).

public class Post
{
    ...

    public Post Body(string body)
    {
        this.BodyValue = body;
        return this;
    }

    public Post Tags(string tags)
    {
        this.TagsValue = tags;
        return this;
    }
}
Returning the same instance of the object we are building after each method call is one of the most common techniques used to write fluent interfaces, it’s called Method Chaining.

Step 5 – Completing the first iteration and running tests

Now, let’s implement the Build method by creating an instance of BlogPost based on the values provided with the fluent interface.

public class Post
{
    ...

    public BlogPost Build()
    {
        var blogPost = new BlogPost();

        blogPost.Title = this.TitleValue;
        blogPost.Body = this.BodyValue;
        blogPost.Tags = this.TagsValue;

        return blogPost;
    }
}

Our test should be green now!

Step 6 – Don’t forget the Author

Here comes the tricky part, let’s add the Author now…

If we continue what we’ve been doing with the Title, Body and Tags methods, we should make Author return an instance of Post; but if we do that, we should put the methods Name, Email and Twitter in the Post class, and we don’t want that. What we want is having those methods in a separate class, so instead of returning an instance of Post we are going to return an instance of a new class (AuthorSpec). Let’s try it.

public class Post
{
	...

	public AuthorSpec Author()
	{
		var authorSpec = new AuthorSpec();
		return authorSpec;
	}
}

public class AuthorSpec
{
	public AuthorSpec Name(string name)
	{
		this.NameValue = name;
		return this;
	}

	public AuthorSpec Email(string email)
	{
		this.EmailValue = email;
		return this;
	}

	public AuthorSpec Twitter(string twitter)
	{
		this.TwitterValue = twitter;
		return this;
	}
}

Now let’s create a new test method, adding the “Author” code:

public void ConfiguringPostWithAuthor()
{
	var expected = new BlogPost();
	expected.Title = "How to build a basic fluent interface in 8 steps";
	expected.Body = "<html>...</html>";
	expected.Tags = "Fluent API, Internal DSL";
	var author = new Author();
	author.Name = "John Doe";
	author.Email = "johndoe@email.com";
	author.Twitter = "@johndoe";
	expected.Author = author;

	var post = Post
		.Title("How to build a basic fluent interface in 8 steps")
		.Body("<html>...</html>")
		.Author()
			.Name("John Doe")
			.Email("johndoe@email.com")
			.Twitter("@johndoe")
		.Tags("Fluent API, Internal DSL");
	BlogPost actual = post.Build();

	Assert.IsTrue(CheckEquivalence(expected, actual));
}

But wait! The compiler says that the Tags method is not recognized as a member of the AuthorSpec class. That’s because we are calling Tags over the return value of Twitter, which is an AuthorSpec object instead of a Post object.

When writing fluent APIs, it is very common to bump into this kind of problems, the most used solutions for this are ending the Author specification with an “end” method, or passing a nested closure to the Author method.

Step 7 – Closing the Author specification and running more tests

Option A, using an “End” method.

public class Post
{
    ...

    public AuthorSpec Author()
    {
        this.authorSpec = new AuthorSpec(this);
        return authorSpec;
    }
}

public class AuthorSpec
{
    public AuthorSpec(Post parent)
    {
        this.parent = parent;
    }

    ...

    public Post End()
    {
        return this.parent;
    }
}

How it looks like:

var post = Post
    .Title("How to build a basic fluent interface in 8 steps")
    .Body("<html>...</html>")
    .Author()
        .Name("John Doe")
        .Email("johndoe@email.com")
        .Twitter("@johndoe")
        .End()
    .Tags("Fluent API, Internal DSL");

Option B, using a nested closure.

public class Post
{
    ...

    public Post Author(Action<AuthorSpec> spec)
    {
        this.authorSpec = new AuthorSpec();
        spec(authorSpec);
        return this;
    }
}

How it looks like:

var post = Post
    .Title("How to build a basic fluent interface in 8 steps")
    .Body("<html>...</html>")
    .Author(a => a
        .Name("John Doe")
        .Email("johndoe@email.com")
        .Twitter("@johndoe"))
    .Tags("Fluent API, Internal DSL");

I personally prefer option B. I think it’s easier to read and the programmer doesn’t have to remember calling the “End” method.

After implementing one of these options, our test should be green.

Step 8 – Stepping into dangerous territories

Let’s imagine we want to force people using our API to set the author name first and then configure the email address or the twitter account, but not both (exclusive or). We can make use of interfaces to show only a filtered subset of the methods in the method chain.

public class Post
{
    ...

    public Post Author(Action<IAuthorSpecAfterCreation> spec)
    {
        this.authorSpec = new AuthorSpec();
        spec(authorSpec);
        return this;
    }
}

public interface IAuthorSpecAfterCreation
{
    IAuthorSpecAfterName Name(string name);
}

public interface IAuthorSpecAfterName
{
    IAuthorSpecAfterEmailOrTwitter Email(string email);
    IAuthorSpecAfterEmailOrTwitter Twitter(string twitter);
}

public interface IAuthorSpecAfterEmailOrTwitter
{
}

public class AuthorSpec : IAuthorSpecAfterCreation,
  IAuthorSpecAfterName, IAuthorSpecAfterEmailOrTwitter
{
    public IAuthorSpecAfterName Name(string name)
    {
        this.NameValue = name;
        return this;
    }

    public IAuthorSpecAfterEmailOrTwitter Email(string email)
    {
        this.EmailValue = email;
        return this;
    }

    public IAuthorSpecAfterEmailOrTwitter Twitter(string twitter)
    {
        this.TwitterValue = twitter;
        return this;
    }
}
This technique can become very useful, but the complexity of the API can grow quickly with so many interfaces here and there… use it with precaution.

The complete sources can be grabbed from here.

That’s it, I hope this helps :)


Viewing all articles
Browse latest Browse all 2

Latest Images

Trending Articles





Latest Images