技术开发 频道

用Ruby创建领域特定语言(DSL)


更复杂的DSL

我们现在来实现更复杂的DSL 特性。 不仅仅操作数据,而且要执行更具体的行为。 想象一下我们厌烦了在每次开始一个新的项目的时候,手动的生成一个通用的目录集和文件集。 如果Ruby可以帮我们做这些就好了。更进一步,如果我们有一个小的DSL使得我们可以直接修改项目目录结构而不用去编写低级的代码,岂不更好。

我们现在开始为这个问题定义一个DSL。 下面的文件是这个DSL 的0.01 版本:
% cat project_template.dsl create_project do dir "bin" do create_from_template :exe, name end dir "lib" do create_rb_file name dir name do create_rb_file name end end dir "test" touch :CHANGELOG, :README, :TODO end

在这个DSL文件里,我们生成了一个项目,在其中加了三个目录和三个文件。在’bin ‘ 目录中,我们使用’:exe’模板生成了一个与项目名字同名的可执行文件。在’lib’目录,我们生成了一个.rb 文件和一个目录, 都与项目名字同名。在这个内部子目录中,又生成另一个与项目名字同名的’.rb’ 文件。最后,在项目优异目录下,生成了一个’test’目录,和三个空文件。

这个DSL需要的方法(method)是:create_project,dir,create_from_template,create_rb_file, 以及 touch。 让我们逐个的看一下这些方法。

方法create_project是最外层的壳(wrapper)。 这个方法提供了一个作用域让我们将所有的DSL代码都放在一个块(block)中。(完整的代码列表请看文章的最后)

def create_project()

yield

end

方法dir 完成实质性的工作。该方法不仅仅生成目录,而且将当前的工作目录保存在实例变量 @cwd中。 在这里,使用ensure 来保证@cwd 的始终有正确的值。

def dir(dir_name) old_cwd = @cwd @cwd = File.join(@cwd, dir_name) FileUtils.mkdir_p(@cwd) yield self if block_given? ensure @cwd = old_cwd end

方法touch 和 create_rb_file 基本是一样的,除了后面一个给文件名加了一个后缀’rb’以外。 这些方法可以接受一个或多个文件名,这些名字可以是字符串或符号(symbols)。

def touch(*file_names)

file_names.flatten.each { |file|

FileUtils.touch(File.join(@cwd, "#{file}"))

}

end

最后,方法create_from_template 是一个粗略的例子用于说明怎么样可以在一个DSL中实现一些实际的功能。(请看代码的完整列表)

为了运行这些代码,我们构建了一个小的测试应用。

% cat create_project.rb require 'project_builder' project_name = ARGV.shift proj = ProjectBuilder.load(project_name) puts "== DIR TREE OF PROJECT '#{project_name}' ==" puts `find #{project_name}`

运行结果是:

% ruby create_project.rb fred == DIR TREE OF PROJECT 'fred' == fred fred/bin fred/bin/fred fred/CHANGELOG fred/lib fred/lib/fred fred/lib/fred/fred.rb fred/lib/fred.rb fred/README fred/test fred/TODO % cat fred/bin/fred #!/usr/bin/env ruby require 'rubygems' require 'commandline require 'fred' class FredApp < CommandLine::Application def initialize end def main end end#class FredApp

哇!工作得很好。 并且没费多少力气。


总结
我做过的很多项目要求一个非常详细的控制流描述。 在每个项目中,这常常让我停下来并思考怎么将这些详细的配置数据引入到应用(application)中。 现在,Ruby作为一个DSL,几乎是最适合的,而且常常可以非常高效和快速的解决问题。

在培训Ruby 的时候,我会让整个班级用以下方法来解决问题,我们先用英语来描述问题,然后用伪代码,然后用Ruby。但是,在某些情况下,伪代码就是合法的 Ruby 代码。 我认为,Ruby的高度可读性使得 Ruby是一个可用做DSL的理想语言。 当Ruby 为更多的人所了解, 用Ruby 写的DSL 将成为一个与应用通信的流行的方式

项目 ProjectBuilder DSL 的代码列表:

% cat project_builder.rb require 'fileutils' class ProjectBuilder PROJECT_TEMPLATE_DSL = "project_template.dsl" attr_reader :name TEMPLATES = { :exe => <<-EOT #!/usr/bin/env ruby require 'rubygems' require 'commandline require '%name%' class %name.capitalize%App < CommandLine::Application def initialize end def main end end#class %name.capitalize%App EOT } def initialize(name) @name = name @top_level_dir = Dir.pwd @project_dir = File.join(@top_level_dir, @name) FileUtils.mkdir_p(@project_dir) @cwd = @project_dir end def create_project yield end def self.load(project_name, dsl=PROJECT_TEMPLATE_DSL) proj = new(project_name) proj = proj.instance_eval(File.read(dsl), dsl) proj end def dir(dir_name) old_cwd = @cwd @cwd = File.join(@cwd, dir_name) FileUtils.mkdir_p(@cwd) yield self if block_given? ensure @cwd = old_cwd end def touch(*file_names) file_names.flatten.each { |file| FileUtils.touch(File.join(@cwd, "#{file}")) } end def create_rb_file(file_names) file_names.each { |file| touch(file + ".rb") } end def create_from_template(template_id, filename) File.open(File.join(@cwd, filename), "w+") { |f| str = TEMPLATES[template_id] str.gsub!(/%[^%]+%/) { |m| instance_eval m[1..-2] } f.puts str } end end#class ProjectBuilder # Execute as: # ruby create-project.rb project_name
0
相关文章