实例讲解 Ruby DSL

Ruby 语言的一大特色就是其非常强大 DSL 能力。

那什么是 DSL 呢?英文全称为 "Domain-Specific Language", Martin Fowler 对其的定义为:

Domain-specific language (noun): a computer programming language of limited expressiveness focused on a particular domain.

大家可以想象一下,一门用来解决特定领域问题的语言,它相对于通用领域的语言,所面对的问题是有限的,因此相对而言,领域特定语言的表达是更简洁的,因为使用领域特定语言就相当于默认已经给定了上下文。

领域特定语言分为外部 DSL 和内部 DSL,我们要讲解的 Ruby 语言编写的 DSL 就属于内部 DSL。

上面讲过了,DSL 是用于解决特定领域问题的语言,那什么是特定领域呢?举个例子,Rails 可以说就是一门 DSL , 因为它就是 Web 开发这一特定领域的一套解决方案。

如果按照 Rails 给定的思路写程序,那么就能用较少的代码,完成更多的工作。

下面我会以命令行应用的 UI 为例来介绍一下 DSL 的用法。

领域问题

要使用领域特定语言, 就一定要知道 领域问题 是什么。

以构建命令行应用为例,要面对的一个领域问题就是 UI 的构建。

命令行的表现能力是有限的,因此对于命令行的 UI, 基本也有一套固定的思路。

和 RESTFul 有一套固定格式类似,命令行应用也是有一套自己的参数格式的。

一个命令后应用可以被分为 Executable Options Arguments 这么几个部分。

先以下面这个命令行应用为例子:

grep --ignore-case -r "some string" /tmp

Executable

grep 也就是被执行的程序

Options

--ignore-case-r ,前者是 long-form ,后者是 short-form 。同一个 option 可以同时拥有 long-form 和 short-form 两种写法。

Arguments

some string/tmp

Command

实际上更复杂的命令行程序还有 Command 这个概念,不过这里不作讨论。

我们的任务

解决命令行应用的 help UI 的构建问题,设定好 options 后,可以方便的打印出各项 options 的用法。

非 DSL 的写法

这里不做具体实现了,就写点伪代码。

一般的思路无非就是:

# mygrep
options = {
  ignore_case: {"-i", "--ignore-case", true, "Perform case insensitive matching."}
  recurse: {"-r", "--recursive", false, "Recursively search subdirectories listed."}
}

class OptionsParser
  def initialize(options)
    # ... parse
  end

  def to_s
    puts "Usage: mygrep [options]"
    options.values.each do |opts|
      puts "\t #{opts[0]}, #{opts[1]} \t #{opts[2]}"
    end
  end
end

创建一个散列表来储存 options 的信息,然后解析之,最后打印出来。

ruby mygrep.rb
Usage: mygrep [options]
    -i, --ignore-case                Recursively search subdirectories listed.
    -r, --recursive                  Perform case insensitive matching.

DSL 的写法

先看看 Ruby 标准库自带的 OptionParser 是怎么做的

# mygrep.rb

# !/usr/bin/env ruby
require 'optparse'
options = {}
option_parser = OptionParser.new do |opts|
  options[:ignore_case] = false
  opts.on('-i', '--ignore-case', 'Recursively search subdirectories listed.') do
    options[:ignore_case] = true
  end

  options[:recursive] = false
  opts.on('-r', '--recursive', 'Perform case insensitive matching.') do
    options[:recursive] = true
  end
end.parse!

运行结果:

ruby mygrep.rb -h
Usage: mygrep [options]
    -i, --ignore-case                Recursively search subdirectories listed.
    -r, --recursive                  Perform case insensitive matching.

怎么样,是不是可读性上了一个档次。

创建自己的 DSL

在下面这个例子中, 我们要创建一个用于 "模拟" git 提交记录的 DSL, 类似于 git 中的 Lorem Ipsum.

比如说为了讲解 git 的分支使用方式, 我可能需要一个用于教学的 git 仓库, 如果真的自己手动一次一次地提交, 或者写成脚本都是很枯燥乏味的. 如果有如下的方式来方便地构建一组提交记录, 那想必是极好的,DSL 的写法如下:

# git-dsl.rb
GitLorem.new do
  master do
    mentor  "init rails project"
    student "update README"

    layout do
      mentor "import reactjs"
      student "import bootstrap"
    end

    devise do
      student "init devise"
      mentor "add devise user model"
    end

    mentor "add heroku config"
  end
end

可以看到 DSL 的表达力非常强,一个产品的开发流程就这样展示出来了。

下面就是具体的实现,这一定不是最好的实现,但足够理解了

class GitLorem
  attr_accessor :branch_names

  def initialize(&block)
    @branch_names = []
    instance_eval(&block)
  end

  private
  def current_branch_name
    @branch_names.last
  end

  def method_missing(name, *args, &block)
    if block_given?
      @branch_names << name
      branch_name = name
      instance_eval(&block)
    else
      commit(name, current_branch_name, args.first)
    end
    @branch_names.pop if branch_name == current_branch_name
  end

  def commit(committer, branch, message)
    puts "提交者: #{committer} 在分支 #{branch} 创建了提交: #{message}"
  end
end

运行结果如下:

ruby git-dsl.rb
提交者: mentor 在分支 master 创建了提交: init rails project
提交者: student 在分支 master 创建了提交: update README
提交者: mentor 在分支 layout 创建了提交: import reactjs
提交者: student 在分支 layout 创建了提交: import bootstrap
提交者: student 在分支 devise 创建了提交: init devise
提交者: mentor 在分支 devise 创建了提交: add devise user model
提交者: mentor 在分支 master 创建了提交: add heroku config

总结

限于篇幅,本文到此为止,有兴趣的同学可以自己实现 commit 方法,让这段代码可以变成真正有用的脚本。 最后引用《Ruby元编程》中的一句话:

根本没有什么元编程,从来只有编程而已。