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# : Page 2

Domain specific languages have been around since Lisp, and abound in the Unix world of "little languages." A convergence of research has recently brought domain specific languages to the forefront of both language and API design.


advertisement

A .NET Bakery DSL

Suppose you run a bakery, and are in cutthroat competition with the bakery across the street. To remain competitive, you must have flexible pricing rules for assets such as day-old bread (because every time you change your prices, the guys across the street do too). The driving force is flexible business rules.

To that end, you create the idea of discount rules based on customer profiles. The profiles describe customers, and you offer base discount incentives on those profiles. You need to be able to define and redefine these rules at the drop of a hat. To solve the problem, you can compose a couple of DSLs.

A Profile Fluent Interface

The first DSL addresses Profiles. Here's a unit test for the syntax I want:

[Test] public void simple_profile() { Profile p = new Profile(); p.member .frequency(5) .monthlySpending(100); Assert.IsTrue(p.Member); Assert.AreEqual(p.Frequency, 5); Assert.AreEqual(p.MonthlySpending, 100); }

The source for the Profile class appears in Listing 1.

The Profile class uses a DSL technique called member chaining. Member chaining refers to methods and/or properties created with sentence composability in mind. From a C# standpoint, it's as simple as creating properties and methods that return the host object, or this, rather than a more typical return value. This example plays simple games with case: The member chained methods start with lower case letters (having capital letters embedded within sentences would look a little odd) and the "normal" properties use the standard convention. Using syntactic tricks like this is fairly common in the DSL world; it's a technique to bend the language to make it more readable.

Ideally, you want to remove as much syntactic noise as possible, yet a little bit still lurks in the constructor invocation: You must create a new Profile object before allowing the fluent methods to engage. One way to solve this is to create a static factory method on the class that serves as the first part of the chain. In Listing 1, that's the describedAs method within Profile :

static public Profile describedAs { get { return new Profile(); } }

The factory method allows the consumption of the fluent interface to be more graceful, as illustrated in the unit test shown below:



[Test] public void factory_constructor() { Profile p = Profile.describedAs .member .frequency(20) .monthlySpending(150); Assert.IsTrue(p.Member); Assert.AreEqual(p.Frequency, 20); Assert.AreEqual(p.MonthlySpending, 150); }

Notice that using chained properties violates a common rule of properties, the "Command Query Rule," which says that get properties shouldn't make any modifications to the underlying object. However, to make this style of DSL work, you need to ignore the Command Query rule and allow get properties to set an internal field value and still return this.

A Discount Fluent Interface

From a technical standpoint, the Discount implementation looks pretty much like Profile, so I won't show most of the code. Here's the unit test that demonstrates the use of the class:

[Test] public void get_discount_based_on_profile() { Discount discount = Discount .basedOn(Profile.describedAs .member .monthlySpending(100) .frequency(20)) .forMembership(10.0) .forMonthlySpending(75, 5.0) .forNumberOfVisits(10, 10.0); Assert.AreEqual(25.0, discount.DiscountAmount); }

In this example, the Discount class relies on the Profile (created via the fluent interface described above). The other part of the Discount class sets threshold values based on the Profile, determining the amount of the discount for this profile. The implementation of the forXXX methods simply sets an internal value and then returns this which enables fluent interface invocation. Here are the methods:

public Discount forMembership(double discount) { _discountForMembership = discount; return this; } public Discount forNumberOfVisits(int numOfVisits, double discount) { _numberOfVisits = numOfVisits; _discountForVisits = discount; return this; } public Discount forMonthlySpending(int monthlySpending, double discount) { _monthlySpending = monthlySpending; _discountForMonthlySpending = discount; return this; }

The only other interesting part of Discount is the DiscountAmount property, which applies the discount rules to determine an overall discount percentage:

public double DiscountAmount { get { var discount = 0.0; if (_profile.Member) discount += _discountForMembership; if (_profile.Frequency > _numberOfVisits) discount += _discountForVisits; if (_profile.MonthlySpending >_monthlySpending) discount += _discountForMonthlySpending; return discount; } }

The Rule List Class

The last piece is the RuleListChained class that builds lists of Discount rules (see Listing 2).

The only chained method in the class is the addDiscount() method. The unit test (see Listing 3) shows how all the pieces fit together.

Notice how concise the code is. Yes, it looks a little odd if you are primarily used to looking at C# code, but when read from the viewpoint of a non-technical person, there is very little syntactic "cruft."

If you run the test you can see that you do indeed have a discount list. However, a lurking problem exists. Suppose you want to save the rules in a database during the add operation, or even just print out the rule as you add it. Here's a modified version of the addDiscount() method that does just that:

public Discount addDiscount() { var discount = new Discount(); _ruleList.Add(discount); Console.WriteLine(discount); return discount; }

But the results are surprising, as you can see by the error shown in Figure 1.

 
Figure 1. Test Failure: This test failure was caused by inappropriate method chaining.

The Finishing Problem

The problem with using member chaining for the RuleList class is called the finishing problem: When does the call "finish?" When you execute code in the add() method, the rest of the members of the chain haven't executed yet, which causes an exception. How do you solve this problem?

One solution creates a special "finished" method at the end of the chain. For example, here's one way to re-write the test:

list.addDiscount() .withProfile(Profile.describedAs .member .monthlySpending(100) .frequency(20)) .forMembership(10.0) .forMonthlySpending(75, 5.0) .forNumberOfVisits(10, 10.0) .save();

The save() method marks the end of the chain in this case. Adding a finishing marker works, but it harms the fluency of the interface. When you talk to someone, you don't say, "Meet me at the place at 5 PM—SAVE!" Instead, chain-finishing should just work.

As an answer to this particular problem, you can use an alternative resolution technique with nested methods. Instead of implementing the add() method as a chained method, you'll supply context by changing it to a more traditional method call, leaving the other chained methods in place.

public RuleList add(Discount d) { _ruleList.Add(d); return this; }

Changing the chained addDiscount() method to use method nesting lets you to write the rule list definition like the test shown below:

[Test] public void simple_rule_list() { var list = new RuleList(); list.add(Discount .basedOn(Profile.describedAs .member .monthlySpending(100) .frequency(20)) .forMembership(10.0) .forMonthlySpending(75, 5.0) .forNumberOfVisits(10, 10.0)); list.add(Discount .basedOn(Profile.describedAs .monthlySpending(50) .frequency(10)) .forMembership(10.0) .forMonthlySpending(75, 5.0) .forNumberOfVisits(10, 10.0)); Assert.AreEqual(2, list.Count); }

This solves the finishing problem by wrapping the chained methods in a nested method invocation. This is quite common in fluent interfaces. In fact, developers use these rules of thumb when building DSLs:

  • Use method chaining for stateless object construction
  • Use nested methods to control completion criteria
This example of fluent interfaces really just scratches the tip of the iceberg of DSL techniques. As you can see, you can stretch C# in interesting ways to create more readable code. A follow-up article will cover additional DSL techniques; in particular the features added to C# on behalf of LINQ that makes it possible to build rich DSLs.



Neal Ford is software architect and meme wrangler at ThoughtWorks, a global IT consultancy with an exclusive focus on end-to-end software development and delivery of large-scale enterprise applications. He has also designed and developed instructional materials, magazine articles, courseware, video/DVD presentations, and is author and/or editor of five books, spanning a variety of technologies. He is an internationally acclaimed speaker, having spoken at over 100 developer conferences worldwide, and delivered more than 600 talks. Check out Neal's web site for more information.
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