Writing a Simple Ingredient
Now that the quantity extension method exists, you can extend this to handle the other methods required by the recipe fluent interface. Readability is one of the goals of a fluent interface; it sounds clumsy to say
4.gram(), instead of the more natural
4.grams().
To add a plural
grams method, simply create another extension method for int:
public static int grams(
this int weight)
{
return weight;
}
Gram isn't the only unit of weight you need to support. To add
pound (and
pounds) extension methods, you need to be able to convert from pounds to grams (because all internal weights appear in grams). To that end, the extension class shown below adds a constant and a couple of new methods:
private const Double GRAMS_IN_POUND = 453.59237;
public static Double pound(this int weight)
{
return GRAMS_IN_POUND * weight;
}
public static Double pounds(this int weight)
{
return GRAMS_IN_POUND * weight;
}
public static Double lb(this int weight)
{
return GRAMS_IN_POUND * weight;
}
public static Double lbs(this int weight)
{
return GRAMS_IN_POUND * weight;
}
To create the most fluent interface possible, you can create methods for
pound,
pounds,
lb, and
lbs. You'll find a pattern occurs frequently when writing fluent interfaces:
Much more work for the developer of the fluent interface means a richer experience for the interface consumer. In this way, building fluent interfaces is exactly like building good APIs. The code above contains a lot of duplication, which you can clean up using well-known refactoring techniques that I won't go into here because doing so wouldn't add anything to the DSL discussion.
Defining grams and pounds covered the weight units. Now, you can tackle the
of part of the DSL, for example:
42.grams().of("Flour");
This extension method ultimately creates an Ingredient. Here's the code for the simple Ingredient class:
public class SimpleIngredient
{
private String _name;
private int _quantity;
public SimpleIngredient(String name)
{
_name = name;
}
public int Quantity
{
get
{
return _quantity;
}
set
{
_quantity = value;
}
}
public String Name
{
get
{
return _name;
}
}
}
To add the
of method, you can add another extension method, shown below:
public static SimpleIngredient of(
this int quantity, String name)
{
var i = new SimpleIngredient(name);
i.Quantity = quantity;
return i;
}
This method extends the int class (or the boxed up equivalent). It takes an additional parameter indicating the ingredient name, creates a new SimpleIngredient, sets its quantity, and returns the new ingredient. After you add this code the following test passes successfully:
[Test]
public void integer_version_of_OF()
{
var expected = new SimpleIngredient("Flour");
expected.Quantity = 42;
var actual = 42.grams().of("Flour");
Assert.AreEqual(expected.Name, actual.Name);
Assert.AreEqual(expected.Quantity, actual.Quantity);
}
Note that the line that defines the actual result is exactly the target syntax shown at the beginning. You now have a fluent interface for recipes.
Fluent Types
However, nothing is ever quite so simple. It turns out that the above code works great for grams, but not for pounds. Why one but not the other? This illustrates a common problem when writing fluent interfaces, so common in fact that I've given it a name: the "Type Transmogrification Problem." The reason
pounds doesn't work derives from the definition of the
of extension method, specifically, the
of method extends int, but when you call the
pounds method, you're no longer extending an int, because the
pounds method returns a floating point number. Fluent interfaces use quantities frequently, meaning that this problem rears its head often. Worse, in .NET, integers and floating point numbers don't share a common ancestor, so you can't add it at the top of the hierarchy. Although Int32 and Double do have Object in common, it would be bad form to add the gram and pounds methods to every single class!
Within C# (because it's strongly typed), you have to do a little bit of behind the scenes magic. To that end, I'm changing SimpleIngredient to the more general Ingredient class, shown in
Listing 1.
The Ingredient class now adds a flag to help determine the appropriate return type. The
Quantity property now returns an Object, meaning that the code that consumes this fluent interface may have to take that into account. You can lean heavily on autoboxing to handle most of these cases. Also, I've added code to both the
get and
set portions of the
Quantity property to handle the type information correctly. Obviously, this doesn't scale well for applications that consume many types, but in this case, the application needs to support only two types, so the code isn't too ugly. Mostly, I'm fighting with C#'s strong typing, which interferes with the ability to create a really simple fluent interface. However, after this change, the type transmogrification problem is fixed, as shown in the (now successful) test below:
[Test]
public void double_version_of_OF()
{
var expected =
new Ingredient("Test");
expected.Quantity =
3 * GRAMS_IN_POUND;
var actual =
3.pounds().of("Test");
Assert.AreEqual(expected.Name, actual.Name);
Assert.AreEqual(expected.Quantity, actual.Quantity);
Assert.AreEqual(actual.Quantity.GetType(),
typeof(Double));
}