Section 7. Domain-Specific Languages

After cruising through the back roads of Ruby, it is time to get back on the main highway and head back into the heartland of Rails again. It is time to look at the one other aspect of Rails’s success: how Rails writes complicated code using only simple declarations. Let’s examine our final Rubyism of this digital shortcut: domain-specific languages.

I have thus far neglected to touch upon one other important aspect of highways: signage. That would be a serious oversight, because highway signs are one of the most effectively designed examples of communication on the planet. They provide necessary and unambiguous information to motorists who have only seconds to read them. Consequently, they succeed only through simplicity. All highway signs are tediously similar in style. They are made from the same types of reflective material, use similar phrasing (Exit 12 1 mile), and have only a rigorously regulated selection of decorative navigational arrows and facility symbols allowed. Essentially, highway signs are a language constructed from a limited vocabulary of symbols and words. This is easy for motorists to understand because the same language is used everywhere. (An exit is an exit is an exit.) It is also easy for sign creators, who have a consistent way of laying out signs. And it is good for navigation, because a leg of any route can be expressed in the same language. ("Take Route 1 21 miles to Exit 7” is clearer to follow than “Go down the Old Boston Post road until you see intersection with a gas station; take a right to Pawcatuck.”) In computer programming terms, highway signs are an example of a domain-specific language (DSL).

A DSL is a limited computer language subset targeted to a particular use, as opposed to a general-purpose programming language. DSLs provide several important advantages as a programming technique. For starters, because a DSL is targeted toward a specific problem domain, it can be understood even by a user who does not know the broader language. In addition, this specific focus necessitates simpler abstractions and models, which makes it easier to achieve conciseness and consistency. Even for an expert coder, a DSL makes sense because it allows you to code solutions as higher-level concepts rather than more-verbose lower-level implementations. This results in code that is easier to design and maintain. As an example, consider validations. It is far simpler to write

Image

And what if we want to change our validation for e-mail later? It is easier to modify that one line than look for an explicit method declaration. And that is the point of DSLs in a nutshell: They enable you to express complicated operations as much simpler expressions. In essence, they trick you into coding without even realizing it!

Martin Fowler, who coined the term for DSLs, distinguishes between two types of DSLs. External DSLs are those cases where the DSL is a file format that can be parsed into a configuration or operations in the general language. The first file you modify in any Rails coding is an example of this type. Consider the database.yml file:

Image

External DSLs require additional parsing logic to work, which make them harder to create, but that is the limit of what you can do in statically typed languages. However, Ruby’s flexibility also supports a second type of DSL. An internal DSL is a sublanguage specified within the higher-level language itself. Internal DSLs are general-purpose code disguised as a simplified language. Rails uses this approach heavily; indeed, our example of 30.minutes.ago might be considered a DSL because it allows the user to represent and manipulate date intervals using a limited sublanguage of minutes, hours, and days. In fact, Rails itself could essentially be described as a DSL for web applications. Or rather, it is a collection of various separate DSLs focused on specific tasks, including the following:

  • Associations
  • Validations
  • Routing
  • Rendering
  • Builder for XML
  • acts_as_* plug-ins

Let’s take a closer look at associations. Associations are an excellent example of how Rails defines DSLs internally. In Rails, you can specify the relationship between two tables in your database using the has_one, has_many, and belongs_to declarations, as follows:

Image

That belongs_to is one powerful statement. By specifying the relationship of your model to another table, Rails automatically adds all these additional methods to your Review class:

  • user, which returns a reference to the user a review is associated with
  • user=, which can be used to reassign the association to user
  • build_user, which creates a user and links this Review to it
  • create_article, which works like build and automatically saves the association, too
  • Automatic code to update foreign keys in the table when the object is saved

Most useful of these, of course, is the user method, because that enables you to traverse your SQL database as if it were built from object-oriented relationships. There is a lot of magic that is happening behind the scenes when you describe your associations. You just need to make the declaration, and it adds the methods and builds the SQL you need without you needing to think at that level—which is precisely the point of DSLs. By limiting you to a specific sublanguage, they give you the precise high-level way of specifying what you want to do (in this case, describe relationships between tables) without having to code the lower-level implementations.

To see this in action, let’s look at what Rails actually does when you declare an association. Listing 6 is a slightly simplified excerpt of the code within Rails that sets up a belongs_to association to another model. It might still surprise you at this point, but belongs_to is not a keyword or special construct, but just a normal method within ActiveRecord::Base. DSLs are prime examples of how useful Ruby’s metaprogramming can be. In this case, this simple declaration of one line automatically creates methods and adds them to the class. If you need to remove the relationship later, fixing the DSL automatically removes all the associations for the new settings. The result is not only clean but maintainable code.

Listing 6 How Rails Adds an Association

Image

The first line of this listing sets up a reflection object, which is used to store essential information about the table (for example, its name and primary key). This object is then passed into methods that automatically add methods to the class—the mark of metaprogramming in action. We will delve into how that works further, but first notice how the code to add these methods is listed succinctly as accessor methods (user, user=) and constructor methods (create_user, build_user). This example also illustrates another metaprogramming technique when the method calls module_eval, which evaluates a string in the current context. (In this case, the context is Review.) To make this more comprehensible, the actual code being evaluated at this point with our example is this:

Image

This code is then saved into the before_save function, which is called whenever the method Review#save saves the model to the database. The addition of this method makes sure the foreign key association between this object and its owner is correctly stored in the database whenever the object is saved if we change the value of that association.

But how are the methods such as user and create_user added to the model? Let’s look at how belongs_to adds methods to your model. Listing 7 displays the code for association_accessor_methods, the method that sets up the additional methods to access and modify the parent model (user and user= specifically). This code uses another aspect of Ruby’s metaprogramming toolkit—define_method—which can be used to create a method in the calling class using block syntax to represent the method’s body.

Listing 7 Defining Association Methods

Image

I am not going to diagram in detail what all this code is doing, but you might want to see whether you can figure out what the created methods look like. The answer for the first case is as follows:

Image

Notice that the reflection object passed to define_method is used within the method and becomes part of the method’s scope at creation. This is a technique known as a closure. This is truly fascinating stuff, and we’ve looked at only one of the ways Rails builds DSLs. There are many other interesting examples of DSLs to be found, but the important thing to grasp here is how Rails uses metaprogramming to construct its DSLs (and how the flexibility of this metaprogramming allows Rails to provide a DSL for specifying database associations in a way that is concise and consistent [the three principles yet again]). DSL is a powerful technique for containing complexity when building powerful systems.

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

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