Login | Register   
LinkedIn
Google+
Twitter
RSS Feed
Download our iPhone app
TODAY'S HEADLINES  |   ARTICLE ARCHIVE  |   FORUMS  |   TIP BANK
Browse DevX
Sign up for e-mail newsletters from DevX


advertisement
 

Building Domain Specific Languages in C#, Part 2 : Page 2

Building fluent programmatic interfaces lets users make calls in natural ways similar to speech.


advertisement

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)); }



Comment and Contribute

 

 

 

 

 


(Maximum characters: 1200). You have 1200 characters left.

 

 

Sitemap
Thanks for your registration, follow us on our social networks to keep up-to-date