DSLs are a great way to structure code. We all have come to love and appreciate the directives used in Rails:
class Article
belongs_to :blog
has_many :comments
scope :published, -> { where(published: true) }
end
Expressive, clear and concise. In my opinion, it does not get much better than this.
However, providing DSLs introduces boilerplate code. When I am thinking about using this tool to structure code, I tend to think that it is overkill if it is just for a single class. Making up a specific language for just one class does not seem pragmatic. So, in my head, DSL are reserved for Gems or modules that are excessively used.
Nowadays, I use this tool much more liberally. I made a little Gem (dsl_factory) which reduces the boilerplate for defining a DSL. The gem is nothing fancy (~100 lines of code). Yet, it standardizes the way how to define DSLs in my applications.
Let’s look at this example:
class ArticleExporter
# define the DSL
ExportDsl = DslFactory.define_dsl do
string :filename
hash :columns, String, Proc
end
extend ExportDsl
# intent
filename 'out.csv'
column 'Blog', -> { _1.blog.name }
column 'Title', -> { _1.title }
column 'Excerpt', -> { _1.excerpt }
column '# of Comments', -> { _1.comments_count }
# implementation
def write
CSV.open(self.class.filename, 'w') do |csv|
csv << self.class.columns.keys # write headers
Article.find_each do |article|
csv << self.class.columns.values.map { _1.call(article) } # write values
end
end
end
end
Instead of collecting all the columns in the write
method, we can separate intent and implementation.
This is nice to read and easy to maintain.
Here another example, which I used to define a menu structure, which is being used in one form or another throughout the application:
module MenuHelper
# define the DSL
MenuDsl = DslFactory.define_dsl do
array :links do
string :label
callable :url
callable :condition
end
end
extend MenuDsl
# intent
link do
label 'Articles'
url -> { articles_path }
condition -> { true }
end
link do
label 'Review'
url -> { reviews_path }
condition -> { current_user.can_review? }
end
link do
label 'Blog Administration'
url -> { blogs_path }
condition -> { current_user.admin? }
end
end
The DSL definition documents the data structure and the subsequent use of the DSL contract states the intention clearly.
I believe that the usage of DSLs is a useful tool. They are not only reserved for Gems – we can also use them in much smaller scopes.
Happy Coding!
References
- DSL Factory Gem