Chapter 4. Building DSLs

In this chapter

  • How to build an application based on DSLs
  • Creating the Message-Routing DSL
  • Creating the Authorization DSL
  • Creating the Quote-Generation DSL

In this chapter, we’ll look at how to design an application that uses DSLs to do much of its work. We’ll cover several of those DSLs in detail (and leave others for later), and we’ll explore how to build, instantiate, and execute those DSLs in the context of our application. We’re going to focus on two types of DSLs, the ones most commonly used in business applications: technical DSLs and business DSLs.

Technical DSLs are generally used as bootstrapping and configuration mechanisms to make it easier to modify a part of a system. In general, those DSLs enable recurring tasks, usually of a one-off nature. Scripting is a common use for technical DSLs—configuration or build scripts, for example. Combining the power of a flexible language with a DSL designed for the task at hand makes for a powerful tool. Everything you do with a technical DSL can be done using code, but the DSL should make it easier and simpler. A DSL makes it easy to produce a one-off solution to a problem.

Note that a one-off solution isn’t necessarily throwaway code. It’s a solution that you usually need once in an application. Configuring the Inversion of Control container is done once per application, for example, but it’s a critically important part of the application, and it’s something that you’ll modify often as the application grows. Similarly, you tend to have one build script per project, but you want it to be of very high quality.

Business DSLs tend to be more declarative than technical DSLs and often focus on business rules and the actions to be taken when the conditions of those rules are met. A business DSL defines policy, whereas the application code defines the operations. Policy is usually where most changes are made; the operations of a system are mostly fixed.

For example, a business DSL could define the rules for processing orders—rules that would affect the following domain objects:

  • Discounts
  • Payment plans
  • Shipping options
  • Authorization rules

The application code would execute the business DSL scripts in order to get the policy decisions that apply for a particular scenario. It would then take the policy decisions and apply them to the system. We’ll see an example of that when we build the Quote-Generation DSL.

You’re not limited to a single DSL per application. In fact, you’ll probably have several, both technical and business DSLs. Each will handle a specific set of scenarios (processing orders, authorizing payments, suggesting new products, and so on).

Before you can start writing a DSL, though, you need to understand the domain and what kind of DSL you want. That’s what the next section is about.

4.1. Designing a system with DSLs

In the rest of this book, we’ll concentrate on building a system with DSLs in mind. To ensure that the domain is familiar to all, we’ll use an online shop as our example. This will give us a rich domain to play with and allow us to define several different DSLs to show a variety of uses. We’ll probably go a tad overboard with DSL usage in order to demonstrate all sorts of DSLs, so you can take that into consideration. You’ll likely not make use of so many DSLs in a single application in real life.

There are cases where you’ll want to design a system as nothing but a set of DSLs, each handling a specific task, and have the users manage the set of scripts that define the actual behavior of the application. In that type of scenario, you would reverse the usual roles of application code and DSLs—the application code would be focused on infrastructure concerns and the requirements of the DSL. This approach would probably work best in backend processing systems. Creating a UI on top of a DSL is certainly possible, but you’re likely to hit the point of diminishing returns. Good UIs are complex, and a DSL that’s complex enough to create a good UI is a programming language. You would probably want to work with an existing programming language rather than a DSL.

I find that the best approach is to use a DSL to define policy, and application code to define the framework and operations that are executed by the system.

Building such a system turns out to be almost trivial, because all you need to do is write the basic operations that the system is supposed to perform (which are usually fairly well understood), and then you can play around with the policy at will. Those operations, in our sample application, are things such as applying discounts, notifying users, and processing payments.

If you have done your job well, you’ll likely be able to sit down with the customer and define the policy, and let them review it at the same time. How you notify users about an action in the application will rarely change, but when and why you do it may be changed more regularly. The same holds true for discount calculations; how you apply a discount is well known, but the business rules governing when you give a discount change regularly.

We don’t want to deal with UIs or persistence in our example system, so we’ll deal strictly with the backend processing only and fake services for the other parts of the system. We can use DSLs for several purposes in this scenario:

  • Message translation and routing
  • Authorization
  • Quote generation

We’ll start with the easiest, routing and translating messages.

4.2. Creating the Message-Routing DSL

Suppose we have a backend order-processing system that uses messages as the primary means of communication. Several external systems will communicate with the order-processing system, including a web application, business partners, administration tools, and the company’s warehouse system, and all of those will be built by different teams, with different schedules, priorities, and constraints. The backend system is the black hole in the middle, around which all the other systems orbit.

The Message-Routing DSL needs to take an incoming message and dispatch it to the correct handler in the application. Message translation and routing is a simple domain, but it usually looks fairly nasty in code. This is especially true if you want to take versioning into consideration, or if you want to deal with heterogeneous environments.

4.2.1. Designing the Message-Routing DSL

Let’s start with the simplest alternative: an endpoint that can accept JSON-formatted messages and process them. We’ll take a peek at the big picture first, in figure 4.1.

Figure 4.1. Routing messages using DSL

We’ll start from an external application that sends a JSON message to a given endpoint. This endpoint will take the JSON string, translate it to a JSON object, and pass it to the routing module. The routing module will use a set of DSL scripts to decide how to route each message to the business component responsible for handling the message. The business component will perform its job, and can return a reply that will be sent to the client. So far, this is a fairly typical messaging scenario. We only need to add asynchronous messages and we can call ourselves enterprise developers.

Now let’s consider the Message-Routing DSL part of figure 4.1. These are the responsibilities of the routing modules:

  • Accept messages in a variety of formats (XML, JSON, CLR objects, and so on)
  • Translate messages from external representation to internal representation
  • Dispatch internal messages to the appropriate handler

We now know what we need to build; we’re left with deciding on the syntax.

The main reason that we want to use a DSL here is to keep the system flexible and make it easy to add new messages and transformations. This DSL will be used by technical people, most probably the developers on the project. This, in turn, means that we can use a technical DSL here. Each script using this DSL will probably have the following responsibilities:

  • Deciding whether the script can handle the message
  • Transforming the message to the internal message representation
  • Deciding where to dispatch the message
Implementing the Message-Routing DSL

With that in mind, we can start writing a draft of the Message-Routing DSL syntax, as shown in listing 4.1.

Listing 4.1. Initial draft of the Message-Routing DSL
# decide if this script can handle this message
return unless msg.type == "NewOrder" and msg.version == "1.0"

# decide which handle is going to handle it
HandleWith NewOrderHandler:
# define a new list
lines = []
# add order lines to the list
for line in msg.order_lines:
lines.Add( OrderLine( line.product, line.qty ) )
# create internal message representation
return NewOrderMessage(
msg.customer_id,
msg.type,
lines.ToArray(OrderLine) )

This is a good place to start. It’s straightforward to read and to write, and it satisfies all the requirements we have. It’s a highly technical DSL, but that’s fine, because it will be used by technical people.


Note

It’s easy to create technical DSLs, because you don’t have to provide a lot of abstraction over what the language offers. You mostly need to provide a good API and good semantics.


Let’s get to the implementation, starting with the routing part. How do we get the messages in the first place? We need to handle several message types without placing undue burden on the developers. After all, avoiding the need to write adapters or translators for them is exactly why we went with the DSL route. But we also want to keep our DSL implementation as simple as possible. If I need to do things like xmlDocument.SelectNodes("/xpath/query") in the DSL on a routine basis, I probably have an abstraction issue somewhere.

Let’s take a look at figure 4.2, which shows how we can resolve this issue. As you can see, we have a single method here, Route(), that accepts an IQuackFu. We covered IQuackFu in chapter 2—it allows us to handle unknown method calls at runtime in a smart fashion. We used it to build the XMLObject before, and here we can use it to separate the implementation of the message from its structure. This means that we don’t care if the message is XML, JSON, or a plain CLR object. We can treat it as a standard object, and let the IQuackFu implementation deal with the details. This gives us maximum flexibility with a minimum of fuss.

Figure 4.2. The Router class


Note

Route() has a string as its return type. In real-world scenarios, we’d probably want to return a message as well, but for our purposes, a string works just fine.


Now we can get down to building the DSL. We’ll use Rhino DSL to take care of all the heavy lifting of building the DSL. Don’t worry about understanding all of it; the whole of chapter 7 discusses Rhino DSL and how to use it.

We’ll start with a typical first step; defining the implicit base class that will be the basis of our DSL. Listing 4.2 shows the entire code of the base class.

Listing 4.2. The base class for the Message-Routing DSL
/// <summary>
/// This delegate is used by the DSL to return the
/// internal representation of the message
/// </summary>
public delegate object MessageTransformer();

public abstract class RoutingBase
{
protected IQuackFu msg;
public string Result;

public void Initialize(IQuackFu message)
{
msg = message;
}

/// <summary>
/// Routes the current message. This method is overridden by the
/// DSL. This is also where the logic of the DSL executes.
/// </summary>
public abstract void Route();

public void HandleWith(Type handlerType, MessageTransformer transformer)
{
IMessageHandler handler =
(IMessageHandler) Activator.CreateInstance(handlerType);
Result = handler.Handle(transformer());
}
}

How does it work? The Message-Routing DSL script will be compiled into a class that inherits from RoutingBase, and all the code in the script will go into the Route() method, while the msg field will contain the current message during execution.


Implicit Base Class

The Implicit Base Class is one approach to building a DSL. With this approach, we define a base class in the application code, and then a compiler step in Boo will turn the DSL script into a class that’s derived from the defined base class. Hence the base class moniker. The implicit part of the name comes from the fact that there is no reference to the class in the DSL script itself—it’s implicit.

There are three major advantages to this approach.

The first is that we can refer to DSL script instances using the base class, by utilizing standard OOP principals.

The second is that the base class can expose methods and properties that are useful in the DSL. This means that the base class itself composes part of the language that we’re creating. We’ll discuss the mechanics of building this in more detail in chapter 6, but the concept itself is important.

The last advantage is that if the class is implicit, we can replace it. This is extremely helpful when we want to test a DSL or version it.

Using an implicit base class allows us to define the language keywords and constructs (as we did with the Scheduling DSL in chapter 2) easily.


When we execute the Route() method, the DSL code is executed. The second line in listing 4.1 (return if the message is not the expected type or version) checks to see if the message is a match, and if it isn’t, the message is ignored without performing any action.

Then we have the HandleWith NewOrderHandler and the code beneath that. Here we’re using Boo’s ability to infer things for us. In this case, we pass the type name as the first parameter, and Boo will turn that into a typeof(NewOrderHandler) for us. The code underneath the HandleWith line uses implicit blocks to pass the delegate that will transform the message to its internal representation.

We now need a way to compile this DSL. We do it using a DSL engine, as shown in listing 4.3.

Listing 4.3. The Message-Routing DSL engine
public class RoutingDslEngine : DslEngine
{
protected override void CustomizeCompiler(
BooCompiler compiler,
CompilerPipeline pipeline,
string[] urls)
{
// The compiler should allow late bound semantics
compiler.Parameters.Ducky = true;
pipeline.Insert(1,
new ImplicitBaseClassCompilerStep(
// the base type
typeof (RoutingBase),
// the method to override
"Route",
// import the following namespaces
"Chapter4.MessageRouting.Handlers",
"Chapter4.MessageRouting.Messages"));
}
}

Note

The DSL engine is part of the Rhino DSL set of tools, and it’s discussed extensively in chapter 7. A DSL engine contains the configuration required to change the behavior of the Boo compiler to support our DSL.


That’s it. Our DSL is ready to roll, almost. We just need to hook it up to the Router class, as shown in listing 4.4.

Listing 4.4. The Router class handles message dispatch for the application
public static class Router
{
private static readonly DslFactory dslFactory;

static Router()
{
dslFactory = new DslFactory();
dslFactory.Register<RoutingBase>(
new RoutingDslEngine());
}
public static string Route(IQuackFu msg)
{
StringBuilder messages = new StringBuilder();
RoutingBase[] routings =
dslFactory.CreateAll<RoutingBase>(
Settings.Default.RoutingScriptsDirectory
);
foreach (RoutingBase routing in routings)
{
routing.Initialize(msg);
routing.Route();
if (routing.Result != null)
messages.AppendLine(routing.Result);
}
if(messages.Length==0)
{
return "nothing can handle this message";
}
return messages.ToString();
}
}

Listing 4.4 gives us a few concepts to discuss. In the constructor, we create a new DSL factory and register our Message-Routing DSL engine, but the important parts are in the Route(msg) method.

We ask the DSL factory to give us all the DSL instances in a specific folder (CreateAll will return instances of all the scripts in the given path). This is a nice way of handling a set of scripts (though it tends to break down when you have more than a few dozen scripts—at that point, you’ll want better management of them, and we’ll discuss this in chapter 5). We get back an array of RoutingBase instances from CreateAll, which we iterate over and run. This gives all the scripts a shot at handling the message.

The last pieces we’re missing are the JSON endpoint and the JsonMessageAdapter. We’ll start from the endpoint, because this is simple ASP.NET stuff. We create an HTTP handler class that accepts the messages and then sends them to be routed. Listing 4.5 shows how it’s done.

Listing 4.5. The JSON endpoint
public void ProcessRequest(HttpContext context)
{
//verify that we only allow POST http calls
if (context.Request.RequestType != "POST")
{
context.Response.StatusCode = 400;
context.Response.Write("You can only access this URL using POST");
return;
}
// translate from the post body to a JSON object
byte[] bytes = context.Request.BinaryRead(context.Request.TotalBytes);
string json = Encoding.UTF8.GetString(bytes);
JsonSerializer jsonSerializer = new JsonSerializer();
JsonReader reader = new JsonReader(new StringReader(json));
JavaScriptObject javaScriptObject =
(JavaScriptObject)jsonSerializer.Deserialize(reader);

// send the JSON object to be routed
string returnMessage =
Router.Route(new JsonMessageAdapter(javaScriptObject));
context.Response.Write(returnMessage);
}

This code deals mostly with unpacking the data from the request and deserializing the string into an object. The important part happens on the second-last line: we call Router.Route() and pass a JsonMessageAdapter. This class is responsible for translating the JavaScriptObject into an IQuackFu, which is what we expect in the Message-Routing DSL.

The code for JsonMessageAdapter is in listing 4.6.

Listing 4.6. The JsonMessageAdapter implementation
public class JsonMessageAdapter : IQuackFu
{
private readonly JavaScriptObject js;

public JsonMessageAdapter(JavaScriptObject js)
{
this.js = js;
}

public object QuackGet(string name, object[] parameters)
{
object value = js[name];
JavaScriptArray array = value as JavaScriptArray;
if(array!=null)
{
return array.ConvertAll<JsonMessageAdapter>(
delegate(object obj)
{
return new JsonMessageAdapter(
(JavaScriptObject) obj);
});
}
return value;
}
}

This listing only shows the QuackGet() method and ignores QuackSet() and QuackInvoke(), because they aren’t implemented. About the only interesting thing here is how we deal with arrays, because we need to convert them to JsonMessageAdapter arrays.

That’s all, folks. Honest. We need around 200 lines of code to build this, and it takes about an hour or so.

Go back to listing 4.1 and look at the DSL that we wrote. We can now use it to process JSON messages like the one in listing 4.7.

Listing 4.7. A JSON message that can be handled by our DSL
{
type: "NewOrder",
version: "1.0",
customer_id: 15,
order_lines:
[
{ product: 3, qty: 5 },
{ product: 8, qty: 6 },
{ product: 2, qty: 3 },
]
}

Extending this infrastructure—to deal with XML objects, for example—is a simple matter of creating an XmlMessageAdapter (or using the XmlObject that we created in chapter 2) and adding a new endpoint that can accept it.

You have probably figured out that the Message-Routing DSL is a very imperative DSL, but it’s more than just its syntax; it also does a lot. Calling Router.Route() takes care of everything from invoking the DSL logic to selecting the appropriate handlers, executing them, and returning the results. After calling the Message-Routing DSL, there isn’t much left to be done.

In the space of a few pages, we created a DSL, implemented the structure around it, and are ready to put it to use. It wasn’t complex, and we didn’t even have to use any of the advanced options that are at our disposal.

The reason it was so simple is mostly that we can get away with having a very technical language. This means we could utilize the built-in syntactic sugar in Boo to get a nice DSL, but not much more. Nearly everything we did was to create infrastructure code and run the DSL.

The next DSL we’ll build—the Authorization DSL—will have a much higher focus on the language than the infrastructure. The infrastructure is mostly the same from one DSL implementation to the next, so we don’t need to focus on that any longer.

4.3. Creating the Authorization DSL

Most complex applications have equally complex authorization rules, often complex enough to be their own domain. Authorization is a critical chore in an application. You can’t avoid it, but it’s almost always annoying to deal with, and it can be very complex. Worse, any bugs in the security system are critical by definition. Trying to understand why a certain operation was allowed (or denied) can be a complex endeavor.

We’re going to build a DSL for making policy decisions about permissions in an application. Getting security right is important, and security systems also have to be flexible. All in all, it seems like a good fit for a DSL.

This is the specification for our Authorization DSL:

  • Limit the problem domain to reduce complexity.
  • Flexibility is important.
  • Clarity is critical.

The specification could use some more work, but let’s try to define the requirements we have for the Authorization DSL:

  • It must be able to ask the security system if an operation is allowed.
  • It must be able to ask the security system if an operation on a specific entity is allowed.
  • It must output reasonable information from the security system to allow easy debugging.

In this list, an operation is something that the application does that needs to be secured. Viewing customer information or authorizing a client to go above a credit limit are both examples of operations in our online shop example.

I tend to use simple structured strings to represent operations. Here are two examples:

  • "/customer/orders/view"
  • "/customer/beyond_credit_limit/authorize"

If this reminds you of paths in a filesystem, that’s no coincidence. This is a natural way to think about operations in our domain. It’s also a simple approach that supports tooling well.

Enough with the chitchat—let’s see some code.

4.3.1. Exploring the Authorization DSL design

This time, we’ll start from the application and move to the DSL, rather than the other way around. We’ll start with the Authorization class, which is the gateway to our security infrastructure. It’s shown in figure 4.3.

Figure 4.3. The Authorization class, the gateway to the Authorization DSL

The Authorization class contains two methods: one for checking an operation, and the second for checking an operation on an entity. The WhyAllowed() method lets you retrieve the reason for a certain operation being allowed or denied.

One thing to note is that the IsAllowed() methods return a nullable boolean. This allows the method to return null when the security system has no opinion on the subject. If that happens, the application needs to decide whether the operation is allowed by default or is forbidden by default. This is a matter for the business logic specific to each operation and cannot be dictated by the DSL implementation.

Now let’s think about what kind of authorization rules we’re going to need. We’re going to use the Command pattern in the DSL—this is a fairly common approach in DSL building, and implicit base classes will usually implement the command patterns. The Command pattern is a design pattern in which an object represents a single action (a command) in the application. For more details about the Command pattern, see the explanation at the Data & Object Factory (http://www.dofactory.com/Patterns/PatternCommand.aspx). Figure 4.4 shows the AuthorizationRule class.

Figure 4.4. The base class for the Authorization DSL—AuthorizationRule

There are a couple of interesting things to note about the implementation of this class:

  • The CheckAuthorization() method and the Operation property are both abstract, so derived classes (and our DSL) have to implement them.
  • The Allow() and Deny() methods are the only ways for the derived class to affect the state of the rule. Both methods accept a reason string, which means that we’re automatically documenting the reason for the decision.

For the moment, we’ll skip over the DSL implementation (which we’ll get to in the next section). We’ll assume that it exists and look at how we’re going to use it.

Listing 4.8 shows the ExecuteAuthorizationRules() method. It’s a private method in the Authorization class that performs the bulk of the work for the class.

Listing 4.8. ExecuteAuthorizationRules() is the engine for the Authorization DSL
private static AuthorizationResult ExecuteAuthorizationRules(
IPrincipal principal,
string operation,
object entity)
{
// get all the authorization rules
AuthorizationRule[] authorizationRules =
dslFactory.CreateAll<AuthorizationRule>(
Settings.Default.AuthorizationScriptsDirectory,
principal,
entity);
foreach (AuthorizationRule rule in authorizationRules)
{
// check if the current rule operation equals
// the requested operation, we don't care about casing
bool operationMatched = string.Equals(
rule.Operation, operation,
StringComparison.InvariantCultureIgnoreCase);
// move on if this is not so.
if (operationMatched == false)
continue;

// execute the rule
rule.CheckAuthorization();
// return the result if the rule had any.
if (rule.Allowed != null)
{
return new AuthorizationResult(
rule.Allowed,
rule.Message
);
}
}
// return a default (negative) result if
// no rule matched this operation
return new AuthorizationResult(
null,
"No rule allowed this operation"
);
}

By now, you can probably guess what the DSL looks like, right?

Listing 4.9 shows the DSL engine for the Authorization DSL.

Listing 4.9. The Authorization DSL engine implementation
public class AuthorizationDslEngine : DslEngine
{
protected override void CustomizeCompiler(
BooCompiler compiler,
CompilerPipeline pipeline,
Uri[] urls)
{
// The compiler should allow late-bound semantics
compiler.Parameters.Ducky = true;
pipeline.Insert(1,
new ImplicitBaseClassCompilerStep(
// the base type
typeof(AuthorizationRule),
// the method to override
"CheckAuthorization",
// import the following namespaces
"Chapter4.Security"));
}
}

You may have noticed that the AuthorizationDslEngine displays a stunning similarity to the RoutingDslEngine. This is almost always the case, because most of the hard work is done in the Rhino DSL code. This leaves us with what is basically configuration code.

4.3.2. Building the Authorization DSL

Now let’s take a look at our DSL, shall we? We want to implement the following rules:

  • Users can log in between 9 a.m. and 5 p.m.
  • Administrators can always log in.

The script in listing 4.10 satisfies those requirements.

Listing 4.10. A simple authorization script for account login
operation "/account/login"

if Principal.IsInRole("Administrators"):
Allow("Administrators can always log in")
return

if date.Now.Hour < 9 or date.Now.Hour > 17:
Deny("Cannot log in outside of business hours, 09:00 - 17:00")
return

This looks almost boringly standard, right?[1] We make use of the Boo date keyword to reference System.DateTime, which we haven’t seen before, but that isn’t very interesting.

1 The job of an architect is to make everyone in the team expire out of sheer boredom. The more complexity we can shift into the infrastructure, the less complexity we need to deal with during our day-to-day coding.

There is one interesting thing here: the first line isn’t something that we’ve seen so far. We know that we need to provide an implementation of the Operation property, but how can we do it?

It’s done using a macro, which takes the first argument of the macro, generates a property, and returns that argument from the property. The code for this is shown in listing 4.11.

Listing 4.11. OperationMacro creates a property returning its first argument
public class OperationMacro : GeneratePropertyMacro
{
public OperationMacro()
: base("Operation")
{
}
}

The GeneratePropertyMacro that we inherit from is the class that does all the work. We only need to extend it and pass the desired property name. The end result of this line,

operation "/account/login"

is this code:

Operation:
get:
return "/account/login"

And that’s it, more or less. We can now ask the authorization system questions, which will be answered by the DSL. We can execute the code in listing 4.12 to do just that.

Listing 4.12. Using the authorization system
WindowsPrincipal principal = new WindowsPrincipal(
WindowsIdentity.GetCurrent());
bool? allowed = Authorization.IsAllowed(principal, "/account/login");
Console.WriteLine("Allowed login: {0}", allowed);
Console.WriteLine(Authorization.WhyAllowed(principal, "/account/login"));

Note that in listing 4.8, we assumed a denied-unless-allowed policy. This means that our scripts don’t have to explicitly deny anything—they can simply not allow it. This is a nice option to have in certain circumstances.

Let’s try another example to see how useful our DSL is. Suppose we want to specify that only managers can approve orders with a total cost of over $10,000. Listing 4.13 shows the DSL script that validates this rule.

Listing 4.13. Authorization script that ensures only managers can approve costly orders
operation "/order/approve"

if Principal.IsInRole("Managers"):
Allow("Managers can always approve orders")
return

if Entity.TotalCost >= 10_000:
Deny("Only managers can approve orders of more than 10,000")
return

Allow("All users can approve orders less than 10,000")

Tip

Boo supports the use of the underscore character as a thousands separator in integers. This makes it easier to read big numbers.


Listing 4.14 shows how we can use this script.

Listing 4.14. Using the authorization script for approving orders
bool? allowed = Authorization.IsAllowed(principal,
"/order/approve", order);
Console.WriteLine("Allowed login: {0}", allowed);
Console.WriteLine(Authorization.WhyAllowed(principal,
"/order/approve", order));

In only a few pages, we’ve built ourselves a flexible authorization system. It isn’t production-ready yet—we need to add all the usual error handling, edge cases, tuning, and priming—but it’s a great example of using a DSL to easily extend your application.

The Authorization DSL is a more declarative example than the Message-Routing DSL. While the syntax is very imperative, the end result of the Authorization DSL is a value that is then processed by the application.

4.4. The “dark side” of using a DSL

We also need to explore the dark side of using DSLs such as the Authorization DSL. Our system allows us to easily express authorization rules related to business logic, but it suffers from a couple of weaknesses. It doesn’t have a friendly UI; we can’t do much with it except by editing the code.[2] And it doesn’t really permit programmatic modification—if you find yourself generating DSL scripts on the fly, you probably need to rethink your DSL editing strategy.

2 This is not strictly true. You can build a designer for this, but that’s not really what this sort of DSL is meant to do.

Most security systems are based on the concepts of users and roles for a reason. Those are easy to handle UI–wise, and it’s easy to programmatically add a user to a role or remove a user from a role. The same approach won’t work using a DSL. You could certainly use roles in the DSL, and you could do other smart things (the DSL has access to the application, so you could store the state in a database and let the DSL query that). But a good DSL is intentionally limited, to reduce the complexity that you have to deal with.

If a task is awkward to deal with, you build a DSL to handle the complexity. If the DSL is awkward to deal with, you may want to rethink your approach.

Let’s see how we can integrate a DSL with a user interface, shall we? The secret is to separate the responsibilities so the DSL is based on rules, and the rules feed on the data from the UI. The Quote-Generation DSL is a good candidate for a DSL that can accept data from the user. We’ll discuss it next. In chapter 10, we’ll do the reverse, taking the DSL output and working with it in the UI.

4.5. The Quote-Generation DSL

Let’s say that our online shop sells enterprise software, and our customers want to buy our system. The price will vary depending on the options they want, the number of expected users, the underlying platform, the application dependencies, and a host of other things. Generating the quote can get pretty complex. Quote generation is also an extremely fluid field; quotes and the rules governing them change frequently, making this task a good candidate for a DSL.


Note

I used the software sales example here because it’s easy to understand, but you could use the same approach to generate quotes for custom cars, house purchasing, and so on.


Figure 4.5 shows an example of a quote-generation UI for a single application. It doesn’t represent all the options that exist in the application. This is much more than a UI—there is a full-fledged logic system here. Calculating the total cost is the easy part; first you have to understand what you need.

Figure 4.5. The quote-generation UI allows us to pass information from the application to the DSL.

Let’s define a set of rules for the application. It will be clearer when we have the list in front of us:

  • The Salaries module requires one machine for every 150 users.
  • The Taxes module requires one machine for every 50 users.
  • The Vacations module requires the Scheduling Work module.
  • The Vacations module requires the External Connections module.
  • The Pension Plans module requires the External Connections module.
  • The Pension Plans module must be on the same machine as the Health Insurance module.
  • The Health Insurance module requires the External Connections module.
  • The Recruiting module requires a connection to the internet, and therefore requires a firewall from the recommended list.
  • The Employee Monitoring module requires the CompMonitor component.

This example is still too simple. We could probably come up with 50 or more rules that we would need to handle. Handling the second-level dependencies alone (External Connections, CompMonitor, and so on) would be a big task.

Now that we’ve established that this is a complex field, let’s think about how we could utilize a DSL here. We have a set of independent rules that affect the global state, which makes this a great candidate for a DSL. We can define each of the rules in its own script, and execute them as we’d normally do. This gives us the flexibility that we want.

But unlike the previous two examples (the Message-Routing and Authorization DSLs), we aren’t dealing with a primarily technical DSL here. The Quote-Generation DSL is a business-facing DSL. Some of the information may be technical, but a lot of it is related to business requirements, and the DSL will reflect that.

Business-facing DSLs tend to take a bit more work to create than technical DSLs. So far, we haven’t paid much attention to the language itself—it was fairly obvious what it was supposed to be, because the target audience was developers. Now we need to think about it, and we need to explore a few possibilities.

The code in listing 4.15 solves the Quote-Generation DSL problem using the same approach that we have taken so far.

Listing 4.15. A technical DSL for quote generation
if has( "Vacations" ):
add "Scheduling"

number_of_machines["Salary"] = (user_count % 150) +1
number_of_machines["Taxes"] = (user_count % 50) +1

This could work, but trying to express something like “must reside on the same machine as another module” would be complex. The whole thing ends up looking like code, and we won’t gain much in the process.

4.5.1. Building business-facing DSLs

The rules listed previously for our application include a few basic conditionals, numbers of users, and dependencies. We can make them (and any other common requirements) part of our DSL.

Listing 4.16 shows one way to specify the rules from the previous list.


Managing DSL snippets

I’ve mixed a few rules together in listing 4.15 to make it easier to see what’s going on. When the scripts are this small, we’re likely to want to handle them in a way that’s a bit different than the one file per script that we have had so far.

In this case, most rules will end up being a line or two long. Those are what I call snippets—they’re simple to write, and we’re going to have a lot of them. The combination of all those snippets gives us a rich behavior, as all the small pieces come together to perform a single task.

We’ll discuss the management of a system composed of many small snippets in chapter 5.


Listing 4.16. Business-facing, declarative DSL for solving the quote-generation problem
specification @vacations:
requires @scheduling_work
requires @external_connections

specification @salary:
users_per_machine 150

specification @taxes:
users_per_machine 50

specification @pension:
same_machine_as @health_insurance

This looks much better. This is a purely declarative DSL that allows us to specify easily the information that we want, and to defer any decisions or calculations to the engine that runs the application.


Tip

When I originally created this example, I hadn’t intended to create a declarative DSL. But sometimes the model insists on a given approach, and it’s usually worth going along with it to see what the result is.


Let’s explore the implementation of this DSL.[3] The implicit base class for this DSL is the QuoteGeneratorRule, shown in figure 4.6.

3 I am not going to implement the quote-generation engine, because this is both complex and beyond the scope of the book.

Figure 4.6. Implicit base class for the Quote-Generation DSL

We need to note a few things about QuoteGeneratorRule. The first is that we use a strange naming convention for some of the methods (requires, same_machine_as, specification, and users_per_machine do not follow the standard .NET naming convention). This is the easiest way to get keywords in the DSL. We can take more advanced routes, like convention-based mapping, but doing so requires us to perform our own method lookup, and that isn’t trivial. Changing the naming convention on the implicit base class is the easiest, simplest solution for the problem. Note that the specification() method accepts a delegate as the last parameter, so we’re using anonymous blocks again, like we did in the Message-Routing DSL Handle method.

The second thing to note is all the @ signs scattered through the DSL (listing 4.16). Those are called symbols, and they translate to string literals without the annoying quotes. This may seem like a small thing, but it significantly enhances the readability of the language.

Boo doesn’t support symbols natively, but they’re trivial to add. In fact, the functionality is already there in Rhino DSL, which makes our DSL engine boring once again. Listing 4.17 shows what we need to add to the engine to support symbols.

Listing 4.17. Adding support for symbols
public class QuoteGenerationDslEngine : DslEngine
{
protected override void CustomizeCompiler(
BooCompiler compiler,
CompilerPipeline pipeline,
Uri[] urls)
{
pipeline.Insert(1,
new ImplicitBaseClassCompilerStep(
typeof(QuoteGeneratorRule),
"Evaluate",
"Chapter4.QuoteGeneration"));
// add symbols support
pipeline.Insert(2, new UseSymbolsStep());
}
}

The last thing that’s worth noting about the QuoteGeneratorRule class is that it accepts a RequirementsInformation class. This allows it to understand what context it runs in, which will be important later on.

The QuoteGeneratorRule class is responsible for building the object model that will later be processed by the quote-generation engine. We aren’t going to write a quote-generation engine, because it is not fairly simple to do, and building it requires absolutely no DSL knowledge. Listing 4.18 shows the implementation of a few of the methods in the QuoteGeneratorRule class.

Listing 4.18. A few interesting methods from the QuoteGeneratorRule class
public void specification(string moduleName, Action action)
{
this.currentModule = new SystemModule(moduleName);
this.Modules.Add(currentModule);
action();
}
public void requires(string moduleName)
{
this.currentModule.Requirements.Add(moduleName);
}

The end result of evaluating a rule is an object model describing the requirements for a particular quote. For each module, we have a specification that lists what the required modules are, what other modules must run on the same machine, and how many users it can support. A different part of the application can take this description and generate the final quote from it. This DSL was pretty easy to build, and we have a nice syntax, but why do we need a DSL for this? Isn’t this a good candidate for using XML?

4.5.2. Selecting the appropriate medium

We could have expressed the same ideas with XML (or a database) just as easily as with the DSL. Listing 4.19 shows the same concept, expressed in XML.

Listing 4.19. The Quote-Generation DSL expressed in XML
<specification name="vacation">
<requires name="scheduling_work"/>
<requires name="external_connections"/>
</specification>

<specification name="salary">
<users_per_machine value="150"/>
</specification>

<specification name="taxes">
<users_per_machine value="50"/>
</specification>

<specification name="pension">
<same_machine_as name="health_insurance"/>
</specification>

We have a one-to-one mapping between the DSL and XML. The Quote-Generation DSL is a pure declarative DSL, which means that it only declares data. XML is good for declaring data, so it isn’t surprising that we’d have a good match between the two.

Personally, I think the DSL syntax is nicer, and the amount of work it takes to get from a DSL to the object model is small compared to the work required to translate from XML to the same object model. But that’s a personal opinion. A pure declarative DSL is comparable to XML in almost all respects.

It gets interesting when we decide that we don’t want a pure declarative DSL. Let’s add a couple of new rules to the mix, shall we?

  • The Pension Plans module must be on the same machine as the Health Insurance module if the user count is less than 500.
  • The Pension Plans module requires a distributed messaging backend if the user count is greater than 500.

Trying to express that in XML can be a real pain. Doing so would involve shoving programming concepts into the XML, which is always a bad idea. We could try to put this logic into the quote-generation engine, but that’s complicating it for-no good reason.

Using our DSL (with no modification), we can write it as shown in listing 4.20.

Listing 4.20. Dealing with business logic in the DSL
specification @pension:
if information.UserCount < 500:
same_machine_as @health_insurance
else:
requires @distributed_messaging_backend

This is one of the major benefits of using DSLs—they scale well as the application complexity grows.

Now all we’re left with is writing the backend for this, which would take the data we’ve built and generate the pricing. That’s a simple problem, with all the parameters well specified. In fact, throughout the whole process, there isn’t a single place where there’s overwhelming complexity. I like that.


Code or data?

Some people attempt to use DSLs in the same way they would use XML. They use them as data storage systems. Basically, the DSL is relegated to being a prettier version of XML.

From my point of view, this is a waste of time and effort. It’s the ability to make decisions that makes DSLs so valuable. Making all the decisions in the engine code will complicate the engine with all the decisions that we may ever need to make. Putting the decisions in the DSL and feeding the final result to the engine means that the engine is much simpler, the DSL scripts are not more complex, and we still get the flexibility that we want.


4.6. Summary

We started this chapter by defining a domain model, and then we built a DSL or three, representing three different approaches to building DSLs.

  • The Message-Routing DSL is an example of an imperative DSL. It’s executed to perform some goals.
  • The Authorization DSL is a more declarative example (but still mostly imperative). It’s executed to produce a value, which is later used.
  • The Quote-Generation DSL is a mostly declarative example. It produces an object graph that’s later taken up by the processing engine.

We’ve also seen why such DSLs are useful: they can contain logic, which affects the outputted object graph.

We also have another observation to make. Consider the following methods:

  • QuoteGeneratorRule.Evaluate()
  • RoutingBase.Route()
  • AuthorizationRule.CheckAuthorization()

As you would discover if you could refer to The Illustrated Design Patterns Spotter Guide,[4] we have a match: the Command pattern. As a direct result of using the Implicit Base Class approach, we get an implementation of the Command pattern. This means we get a lot of the usual strengths and weaknesses of using the Command pattern, and it also means that the usual approaches to solving those issues are applicable.

4 Such a book doesn’t, unfortunately, exist.


Note

I tend to use the Command pattern often, even outside of writing DSLs. It’s a good way to package functionality and handle complexity in many areas.


In section 4.1, we talked about building a system using DSLs, and we did so by creating three DSLs. In many applications, you won’t have a single DSL, but several, each for a particular domain and task. You can use the same techniques (and sometimes similar syntax) to tailor a language to its particular domain. All of those languages work together to create a single application.

We’ve now built our share of DSLs, and even written a DSL script or two. But we haven’t talked about how to manage them in any meaningful way. We’ve focused on the details, so now it’s time to take a broader look and see how to make use of a DSL in an application. In the next chapter, we’ll look at how to integrate a DSL with an application, develop with it, and go to production with it. We’ll look at the overall development lifecycle.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset