Victor is a ruby developer at Nulogy. He has worked a lot with Java and Ruby platforms. Being a big fan of domain specific languages he likes to blog about implementing them using Groovy, Ruby or Clojure. Victor is a DZone MVB and is not an employee of DZone and has posted 43 posts at DZone. You can read more from them at their website. View Full User Profile

Series: DSLs in Groovy, Part: 2

02.17.2011
| 7138 views |
  • submit to reddit

In my previous post about creating domain specific languages in Groovy I showed how easy it is to write a DSL specifying who follows whom on Twitter. The result of my work looked like this:

List<User> users = FollowersGraphBuilder.build {
users 'Jim', 'Tom', 'Jane'
user('Jim').follows('Tom').and('Jane')
user('Tom').follows 'Jane'
}

There are two ways to improve this DSL:

  • Using dynamic capabilities of Groovy
  • Using statically typed features of Groovy

It worth to mention that Groovy allows us to use both styles, in opposite to, for example, Ruby. In this post I’m going to show you how to improve that DSL using several dynamic tricks.

To make it look better I’m going to use a pattern called Dynamic Reception. Many dynamic languages allow you to respond to unknown messages. If you call a method that doesn’t exist a special method will be executed. This feature is not new, it was well-known in Smalltalk community a while ago. Groovy has three methods allowing you to respond to unknown message: methodMissing, propertyMissing(for getting), propertyMissing(for setting).

Take a look at this chunk of code:

class FollowersGraphBuilder {
...
def propertyMissing(String name) {
new UserFollowingBuilder(user: getUserByName(name))
}

private getUserByName(String name) {
assert users.containsKey(name), "Invalid user name $name"
users[name]
}
...
}

class UserFollowingBuilder {
User user

def follows(UserFollowingBuilder userBuilder) {
userBuilder.user.addFollower user
this
}

def and(UserFollowingBuilder userBuilder) {
follows userBuilder
}
}

I’m using propertyMissing hook here. Each time I am trying to get any property that doesn’t exist it will invoke propertyMissing method and a builder will be returned. Our DSL looks much better after this adjustment.

List<User> users = FollowersGraphBuilder.build {
users 'Jim', 'Tom', 'Jane'
Jim.follows Tom
Tom.follows Jane
}

You may notice that UserFollowingBuilder plays two roles in our example: an object and a subject. In my view, this is fine for this particular situation. But for more complex scenarios it might cause problems. I would keep an eye on such builders that play too many roles.

This command might be improved too:

users 'Jim', 'Tom', 'Jane'

The first way to make it better is just by removing it. The only thing we would have to change is our propertyMissing method:

class FollowersGraphBuilder {
private users = [:]

def propertyMissing(String name) {
users[name] = users[name] ?: new User(name)
new UserFollowingBuilder(user: users[name])
}
...
}

Now all users are created on demand. I’ve also deleted users methods as we don’t need it anymore. After this adjustment our DSL looks better:

List<User> users = FollowersGraphBuilder.build {
Jim.follows Tom
Tom.follows Jane
}

The problem I see with this approach is that if you have a lot of users you can mistype someone’s name and a new user will be created. Also if we have not only users but other classes in our semantic model it will be very hard to guess which one should be created in our propertyMissing method. That is why I prefer to declare users explicitly.

So I’d definitely leave the declaration of users but I would remove quotes to make our DSL look like this:

List<User> users = FollowersGraphBuilder.build {
users Jim, Tom, Jane
Jim.follows Tom
Tom.follows Jane
}

You might be surprised how easy it is to achieve this behavior in Groovy:

class FollowersGraphBuilder {
private users = [:]

def users(NewUserBuilder... userNames) {
userNames.each {creator ->
users[creator.name] = new User(creator.name)
}
}

def propertyMissing(String name) {
if(users.containsKey(name)){
new UserFollowingBuilder(user: users[name])
} else {
new NewUserBuilder(name: name)
}
}
...
}

And a new builder:

class NewUserBuilder {
String name
}

The way it works right now is very simple. If propertyMissing is invoked and a user with a specified name is not created yet a NewUserBuilder will be returned. If it’s created our old friend UserFollowingBuilder will be returned. It is so simple.

I could return a String instead of a NewUserBuilder but having a class might be handy if I need to store a bit more information than just a name. Also it might be confusing if an exception occurred.

For example,

List<User> users = FollowersGraphBuilder.build {
users Jim, Tom
Jim.follows Tom1
}

If you use NewUserBuilder you would see this error message:

No signature of method: UserFollowingBuilder.follows() is applicable for argument types:(NewUserBuilder)

If you use String the message will be not as good:

No signature of method: UserFollowingBuilder.follows() is applicable for argument types: (String)

Sure, everything is clear in our example, but imagine we have 5 classes in our semantic model + a bunch of builders. Using String in this case will make debugging much longer.

Groovy 1.8 (in particularly, GEP-3) can make you code look even better and you won’t have to do anything:

List<User> users = FollowersGraphBuilder.build {
users Jim, Tom, Jane, Rob
Jim.follows Tom and Jane and Rob
Tom.follows Rob and Jim
}

That is all for today. We’ve taken a look at one of the most coolest features of dynamic languages -pattern Dynamic Reception. But there is another way to achieve this result:

Jim.follows Tom and Jane and Rob
Tom.follows Rob and Jim

And you won’t have to use any dynamic capabilities of Groovy. I’ll show how to do it next time.

 

From http://vsavkin.tumblr.com/post/3254409403/series-dsls-in-groovy-part-2

Published at DZone with permission of Victor Savkin, author and DZone MVB.

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)

Tags: