DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports Events Over 2 million developers have joined DZone. Join Today! Thanks for visiting DZone today,
Edit Profile Manage Email Subscriptions Moderation Admin Console How to Post to DZone Article Submission Guidelines
View Profile
Sign Out
Refcards
Trend Reports
Events
Zones
Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Partner Zones AWS Cloud
by AWS Developer Relations
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Partner Zones
AWS Cloud
by AWS Developer Relations
11 Monitoring and Observability Tools for 2023
Learn more
  1. DZone
  2. Coding
  3. Java
  4. Building an Intuitive DSL in Java

Building an Intuitive DSL in Java

A DSL is more than chaining a few methods together. A great DSL should be human readable, which means considering word order and consistency.

Clayton Long user avatar by
Clayton Long
·
Apr. 18, 17 · Tutorial
Like (11)
Save
Tweet
Share
29.47K Views

Join the DZone community and get the full member experience.

Join For Free

When I started RuleBook, I used just a simple method chaining Domain Specific Language (DSL) that also leveraged Java 8's new Lambda functionality. However, as my desire to enhance the [internal] DSL and create a better experience for the language grew, I found just having a couple of objects that chained their own methods together wasn't enough.

The Order of Words Matters

If the goal of a DSL is to create a human readable English-like language, then even an internal Java DSL that builds a Domain Specific Language out of a General Purpose (GP) language must strive to follow some rules of continuity. Take, for example, the Given/When/Then (GWT) language format associated with Business Driven Development. If Given <Some State> When <Some Condition> Then <Some Action> makes sense. But Then <Some Action> Given <Some State> When <Some Condition> doesn't make as much sense, especially if both statements are equivalent according to a DSL. 

Now, let's throw a few more monkey wrenches into the system. It's pretty common for multiple 'then' statements to be allowed in GWT languages. Also, let's assume that there is an optional 'using' keyword that can be used to constrain the state available to a subsequent 'then' statement (like in RuleBook). Now, even if the language doesn't apply ordering constraints, it becomes pretty clear that the order of the keywords used matters. And since order matters for the language to properly translate into the desired functionality, the language should apply the necessary constraints on that order to guard against unwanted behavior.

Enforcing Order

Let's take a simple GWT format DSL interface that uses method chaining via a builder.

public interface GwtBuilder {
  GwtBuilder given(Data data);
  GwtBuilder when(Condition cond);
  GwtBuilder using(DataFilter filter);
  GwtBuilder then(Action action);
  MyObject build();
}


Using the above interface, both of the following code snippets are valid.

GwtBuilder builder = new GwtBuilderImpl();
MyObject obj = builder
  .then(action1)
  .given(data)
  .then(action2)
  .using(dataFilter)
  .build();
GwtBuilder builder = new GwtBuilderImpl();
MyObject obj = builder
  .given(data)
  .when(condition)
  .using(dataFilter)
  .then(action)
  .build();


However, the first code snippet doesn't make a whole lot of sense. For example, if using() filters data for a then() action, which action is it applied to? Then there's how it reads. "Then some action happens, given some state, then some other action happens using some subset of the state" doesn't exactly have a logical linguistic flow. It would make more sense to have the language enforce some rules like the following:

  1. Only a 'given', 'when', 'using' or 'then' method can be the first method.
  2. Only a 'given', 'when', 'using' or 'then' method can follow a 'given' method.
  3. Only a 'then' or 'using' method can follow a 'when' method.
  4. Only a 'using' or 'then' method can follow a 'using' method.
  5. Only a 'using', 'then' or 'build' method can follow a 'then' method.

Using these rules, the first builder's sequence of method calls becomes invalid. However, this can't be done with a single builder. For each unique rule, a unique builder is required. Therfore, enforcing order using method chaining is done by chaining builders.

public interface GwtBuilder {
  GwtBuilder given(Data data);
  GwtWhenUsingBuilder when(Condition cond);
  GwtWhenUsingBuilder using(DataFilter filter);
  GwtThenBuilder then(Action action);
}


Taking the example a step further, the GwtThenBuilder interface might look like the following:

public interface GwtThenBuilder {
  GwtThenBuilder then(Action action);
  GwtWhenUsingBuilder using(DataFilter filter);
  MyObject build();
}

Consistency Matters Too

What makes something intuitive? Is it a result of natural instinct or conditioning? Let's take driving a car for example. Most of us reading this probably know how to drive a car. But certainly none of us were born knowing how to drive a car. Yet right now, if someone handed us some car keys, we would likely know how to use those keys to drive a car. And that's true for just about any car. It's intuitive because it's known. And similarly, if you know how to drive one car, you know how to drive almost every other car. I imagine if there was a car where the normal rules of how to drive a car didn't apply then not many people would drive it. That's because people gravitate to what's known. And what's known or what can easily be known through conditioning is what we refer to as intutive.

Like driving a car, or the old adage about riding a bike, languages that have a familiar and consistent set of rules can be said to be intuitive. And intutive languages are easier to learn and adopt than unintuitive languages. Domain Specific Languages are no different. So, if you want to create one that is 'intuitive' and easy adopt, keep the options limited, keep its use familiar and keep its ruleset concise.

Using the Tools in the Java DSL Toolbox

There are a few approaches to constructing an internal DSL out of the Java language. The common approaches are:

Method Chaining

GwtBuilder builder = new GwtBuilderImpl();
MyObject obj = builder
  .given(data)
  .when(condition)
  .using(dataFilter)
  .then(action)
  .build();


Method Sequencing

MyObject obj = new MyObject();
GwtDsl gwtDsl = new GwtDslImpl(obj);
gwtDsl.given(data);
gwtDsl.when(condition);
gwtDsl.using(dataFilter);
gwtDsl.then(action);


Nested Method Calls

GwtDsl gwtDsl = new GwtDsl();
MyObject obj = gwtDsl.createMyObject(given(data, when(condition, then(action))));


Lambda Expressions

GwtDsl gwtDsl = new GwtDsl();
MyObject obj = GwtDsl(data -> someAction.perform(data));
//the above line could be a method reference; lambda syntax was used for illustration


When constructing a single statement, method chaining may seem like a great choice for everything. But what happens if you want to combine statements. If you use method chaining for constructing individual statements and then also use it for combining statements then the demaracation between statements can be confusing. Take the following example.

builder.add().given(data1).when(cond1).then(action1).add().given(data2).when(cond2).then(action2);


Here, using one approach to define a statement and another to create a strict separation between statements would make the language easier to read. But this is also where consistency comes into play. You wouldn't want method chaining to be used for creating statements, while method calls are used for separating statements sometimes and have method sequencing used for separating statements other times. Such inconsistencies can make any DSL difficult to learn and use and they are easy traps to fall into.

Summary

Building an intuitive [internal] Domain Specific Language (DSL) takes more than just following commonly accepted development practices, like abstracting the expression language from the API. It requires a thoughtful understanding of how the language should be used and how it should not be used. Attention to things like the order of words and consistency within the language can go a long way towards making a language both usable and easy to understand. 

Domain-Specific Language Java (programming language) Method chaining

Opinions expressed by DZone contributors are their own.

Popular on DZone

  • 10 Easy Steps To Start Using Git and GitHub
  • Little's Law and Lots of Kubernetes
  • Using GPT-3 in Our Applications
  • How To Build an Effective CI/CD Pipeline

Comments

Partner Resources

X

ABOUT US

  • About DZone
  • Send feedback
  • Careers
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 600 Park Offices Drive
  • Suite 300
  • Durham, NC 27709
  • support@dzone.com
  • +1 (919) 678-0300

Let's be friends: