In previous installments, I have begun to introduce the harvesting of domain idiomatic patterns (solutions for urgent business problems) by using domain-specific languages. For this task, DSL work is good because they are concise (with as few noisy syntax as possible) and readable (even by developers), and they stand out from more API-centric code. In the previous issue, I had shown how to use Groovy to build a DSL to take advantage of some of its features. In this section, I'll end the discussion of using a DSL to get idiomatic patterns by showing how to build a more complex DSL in Ruby and using JRuby.
About this series
This series aims to introduce a new perspective on software architecture and design concepts that are often discussed but difficult to understand. With a concrete example, Neal Ford will help you lay a solid foundation for the evolutionary architecture and the agile practices of emergent design. By deferring important architectural and design decisions to the final responsibility, you can prevent unnecessary complexity from reducing the quality of your software project.
Ruby is the most popular language currently used to build an internal DSL. Most of the infrastructure you consider when developing on Ruby is DSL based-ruby on Rails, RSpec, Cucumber, Rake, and many others-because it is subordinate to the host-managed internal DSL. The trendy technology of behavioral-driven Development (BDD) requires a strong DSL base to achieve its popularity. This issue will help you understand why Ruby is so popular among DSL fans.
Open Classes in Ruby
Using an open class to add a new method to a built-in class is a common technique for adding expressiveness to a DSL. In the previous issue, I showed two different syntaxes for open classes in Groovy. In Ruby, you have the same mechanism in addition to using a single syntax. For example, to create a recipe DSL, you need a way to catch the catches. Please consider the DSL fragment in Listing 1:
Listing 1. Target syntax for my Ruby based recipe DSL
recipe = Recipe.new "Spicy bread"
recipe.add 200.grams.of "flour"
recipe.add 1.lb.of "nutmeg"
To make this code executable, I must open the Numeric class to add the gram and LB methods to the number, as shown in Listing 2:
Listing 2. Open Class definition in Ruby
class Numeric
def gram
self
end
alias_method :grams, :gram
def pound
self * 453.59237
end
alias_method :pounds, :pound
alias_method :lb, :pound
alias_method :lbs, :pound
In Ruby, class names must start with uppercase letters, and they are also rules for Ruby constants, which means that each class name is also a constant. When Ruby "sees" the class definition, it looks at whether it has loaded the class on its classpath. Because the class name is a constant, you can only have a class with a given name. If the class is already loaded, the class definition will reopen the class to allow me to make changes. In Listing 2, I reopened the Numeric class (which handles fixed and floating-point numbers) to add the gram and pound methods. Unlike Groovy, Ruby does not have rules that must be invoked with empty parentheses for methods that receive no parameters, which means Ruby does not have to distinguish between properties and methods.
Ruby also includes another handy DSL mechanism: the Alias_method class approach. If you want to improve your DSL fluency as much as possible, it is recommended that you deal with cases of a similar diversity. (If you want to see the effort to make this happen, look at the multiplex code for dealing with the plural model class name in Ruby on Rails.) When I clearly add more than one gram, I don't want to syntactically form a clumsy sentence like Recipe.add 2.gram.of ("flour") in my DSL. The alias_method mechanism in Ruby makes it easier to create alternate names for methods to enhance readability. To do this, listing 2 adds a multiple approach to gram and adds alternate abbreviations and a plurality of versions for pound.