深入探索 Ruby 内部与外部 DSL
1. Ruby 内部 DSL 的诞生与优化
在处理 XML 时,
XmlRipper
是一个实用工具。它的
after
方法可在基于 XPath 的处理完成后运行代码块,这为修改 XML 后打印整个文档提供了便利。
随着使用场景的增加,我们希望让
XmlRipper
脚本更像一种特定领域语言(DSL)。首先,可通过
instance_eval
方法消除代码块中对
XmlRipper
实例(
r
参数)的频繁引用。示例代码如下:
class XmlRipper
def initialize(&block)
@before_action = proc {}
@path_actions = {}
@after_action = proc {}
instance_eval( &block ) if block
end
end
ripper = XmlRipper.new do
on_path( '/document/author' ) do |author|
author.text = 'J.R.R. Tolkien'
end
after { |doc| puts doc }
end
ripper.run( 'fellowship.xml' )
这样,在代码块执行时,
self
就等于新创建的
XmlRipper
实例,可直接调用
on_path
和
after
方法。
进一步优化,可使用
instance_eval
读取文件中的脚本,消除样板代码。示例代码如下:
class XmlRipper
def initialize_from_file( path )
instance_eval( File.read( path ) )
end
end
ripper = XmlRipper.new
ripper.initialize_from_file( 'fix_author.ripper' )
ripper.run( 'fellowship.xml')
更实际的做法是通过命令行参数传递脚本文件和 XML 文件:
r = XmlRipper.new
r.initialize_from_file( ARGV[0] )
r.run( ARGV[1] )
至此,一个新的 Ruby 内部 DSL——
Ripper
诞生了。它基于 Ruby,具有声明式的 XML 处理风格,且无需创建复杂的解析器,Ruby 会提供所需的各种功能。
2. 内部 DSL 的进一步拓展
为了让
Ripper
更易用,可利用元编程技术。例如,使用
method_missing
方法将以
on_
开头的方法名转换为简单的 XPath。示例代码如下:
class XmlRipper
def method_missing( name, *args, &block )
return super unless name.to_s =~ /on_.*/
parts = name.to_s.split( "_" )
parts.shift
xpath = parts.join( '/' )
on_path( xpath, &block )
end
end
这样,
on_document_author { |author| puts author.text }
这样的调用就会被正确处理。
3. 常见 Ruby 内部 DSL 示例
许多看似神奇的 Ruby 工具其实都使用了内部 DSL 技术。例如,RSpec 文件:
describe "Array#each" do
it "yields each element to the block" do
a = []
x = [1, 2, 3]
x.each { |item| a << item }.should equal(x)
a.should == [1, 2, 3]
end
end
这既可以看作是对数组行为的结构化描述,也可以看作是对
describe
和
it
方法的调用。
Rake 也是一个出色的 Ruby DSL 示例,简单的 Rakefile 如下:
task :default => [ :install_program , :install_data ]
task :install_data => :installation_dir do
cp 'fonts.dat', 'installation'
end
task :install_program => [ :installation_dir ] do
cp 'document.rb', 'installation'
end
task :installation_dir do
mkdir_p 'installation'
end
这里的
task
方法用于定义任务和依赖关系,使用哈希字面量语法指定依赖关系非常优雅。
此外,ActiveRecord 模型 API 和迁移也具有 DSL 风格。例如:
class Book < ActiveRecord::Base
has_many :authors
belongs_to :publisher
end
class AddBooks < ActiveRecord::Migration
def self.up
create_table :books do |t|
t.string :title
t.integer :publisher_id
end
end
def self.down
drop_table :books
end
end
4. 内部 DSL 的问题与解决方法
内部 DSL 虽然有用,但也存在一些问题。例如,错误信息可能不明确。当
Ripper
脚本出现错误时,错误信息可能以 Ruby 术语呈现,难以定位问题。可通过给
instance_eval
方法传递第二个参数来解决,示例代码如下:
class XmlRipper
def initialize_from_file( path )
instance_eval( File.read( path ), path )
end
end
这样,错误信息会指向正确的文件。
另外,要避免过度追求酷炫的语法而忽略实际问题。例如,将整个 XPath 语法映射到 Ruby 代码可能会导致代码过于复杂,得不偿失。
5. 外部 DSL 的必要性
当内部 DSL 无法满足需求时,可考虑使用外部 DSL。例如,
Ripper
的语法对于非技术用户来说可能过于复杂,他们希望使用更简单的命令,如
print /document/author
、
delete /document/published
和
replace /document/author Tolkien
。但这些命令不是有效的 Ruby 表达式,内部 DSL 无法处理。
6. 构建外部 DSL
外部 DSL 采用传统的语言构建方法,即设计语法并构建解析器。Ruby 凭借其强大的字符串和内置正则表达式,是构建外部 DSL 的不错选择。
以下是一个简单的
EzRipper
类,用于解析简化的 XML 处理语言:
class EzRipper
def initialize( program_path )
@ripper = XmlRipper.new
parse_program(program_path)
end
def run( xml_file )
@ripper.run( xml_file )
end
def parse_program( program_path )
File.open(program_path) do |f|
until f.eof?
parse_statement( f.readline )
end
end
end
def parse_statement( statement )
tokens = statement.strip.split
return if tokens.empty?
case tokens.first
when 'print'
@ripper.on_path( tokens[1] ) do |el|
puts el.text
end
when 'delete'
@ripper.on_path( tokens[1] ) { |el| el.remove }
when 'replace'
@ripper.on_path( tokens[1] ) { |el| el.text = tokens[2] }
when 'print_document'
@ripper.after do |doc|
puts doc
end
else
raise "Unknown keyword: #{tokens.first}"
end
end
end
使用示例:
# 创建 edit.ezr 文件,内容如下
# delete /document/published
# replace /document/author Tolkien
# print_document
EzRipper.new( 'edit.ezr').run('fellowship.xml' )
为了提供更详细的错误信息,可对
parse_statement
方法进行修改:
def parse_statement( statement )
tokens = statement.strip.split
return if tokens.empty?
case tokens.first
when 'print'
raise "Expected print <xpath>" unless tokens.size == 2
@ripper.on_path( tokens[1] ) do |el|
puts el.text
end
when 'delete'
raise "Expected delete <xpath>" unless tokens.size == 2
@ripper.on_path( tokens[1] ) { |el| el.remove }
when 'replace'
unless tokens.size == 3
raise "Expected replace <xpath> <value>"
end
@ripper.on_path( tokens[1] ) {|el| el.text = tokens[2]}
when 'print_document'
raise "Expected print_document" unless tokens.size == 1
@ripper.after do |doc|
puts doc
end
else
raise "Unknown keyword: #{tokens.first}"
end
end
综上所述,Ruby 内部和外部 DSL 各有优缺点。内部 DSL 基于 Ruby 构建,无需复杂解析器;外部 DSL 则可设计自己的语法,适用于内部 DSL 无法处理的场景。在实际应用中,可根据具体需求选择合适的 DSL 类型。
深入探索 Ruby 内部与外部 DSL
7. 外部 DSL 的优势与特点
外部 DSL 相比内部 DSL 具有一些独特的优势。由于它有自己独立的解析器,不受 Ruby 语法规则的限制,可以设计出更贴合特定领域需求的语法。以
EzRipper
为例,它为非技术用户提供了简单易懂的命令,如
print
、
delete
、
replace
和
print_document
,这些命令更符合用户对 XML 处理的直观认知。
外部 DSL 还能提供更精细的错误控制。通过在解析语句时添加详细的错误检查,如检查命令参数的数量是否正确,可以为用户提供更有针对性的错误信息,帮助他们更快地定位和解决问题。
8. 外部 DSL 的操作流程
下面详细介绍使用
EzRipper
进行 XML 处理的操作流程:
1.
创建脚本文件
:编写一个包含 XML 处理命令的文件,例如
edit.ezr
,文件内容可以如下:
delete /document/published
replace /document/author Tolkien
print_document
-
初始化
EzRipper实例 :在 Ruby 代码中,使用脚本文件的路径初始化EzRipper实例。
ripper = EzRipper.new('edit.ezr')
-
运行处理任务
:调用
run方法,并传入要处理的 XML 文件的路径。
ripper.run('fellowship.xml')
9. 内部与外部 DSL 的对比
为了更清晰地了解内部 DSL 和外部 DSL 的区别,下面通过表格进行对比:
| 对比项 | 内部 DSL | 外部 DSL |
| ---- | ---- | ---- |
| 语法限制 | 受 Ruby 语法规则限制 | 可自定义语法,不受 Ruby 语法约束 |
| 解析器 | 使用 Ruby 解析器 | 需要自己构建解析器 |
| 适用场景 | 适合对 Ruby 熟悉的开发者,处理简单的领域问题 | 适合非技术用户或需要复杂特定语法的场景 |
| 错误信息 | 可能以 Ruby 术语呈现,不够直观 | 可自定义详细的错误信息,更易理解 |
10. 选择合适的 DSL
在实际应用中,选择内部 DSL 还是外部 DSL 取决于多个因素:
-
用户群体
:如果用户是熟悉 Ruby 的开发者,内部 DSL 可能更合适,因为他们可以利用 Ruby 的强大功能和熟悉的语法。如果用户是非技术人员,外部 DSL 提供的简单语法会更受欢迎。
-
语法复杂度
:如果需求的语法可以在 Ruby 语法范围内实现,内部 DSL 是一个不错的选择。但如果需要复杂的、与 Ruby 语法差异较大的语法,外部 DSL 则是更好的方案。
-
开发成本
:内部 DSL 无需构建解析器,开发成本相对较低。而外部 DSL 需要投入更多精力来设计和实现解析器。
11. 总结
Ruby 的内部和外部 DSL 为解决不同领域的问题提供了强大的工具。内部 DSL 利用 Ruby 的灵活性,在不脱离 Ruby 语法的前提下,创建出具有声明式风格的特定领域语言。通过
instance_eval
和
method_missing
等技术,可以不断优化和拓展内部 DSL 的功能。
外部 DSL 则为那些无法用 Ruby 语法直接表达的需求提供了解决方案。通过构建独立的解析器,可以设计出更符合特定领域需求的语法,为用户提供更友好的操作体验。
在实际开发中,开发者应根据具体的需求、用户群体和开发成本等因素,选择合适的 DSL 类型,以达到最佳的开发效果。
下面是
EzRipper
处理 XML 的流程图:
graph TD;
A[创建脚本文件] --> B[初始化 EzRipper 实例];
B --> C[运行处理任务];
C --> D[解析脚本文件];
D --> E{判断命令类型};
E -- print --> F[打印元素文本];
E -- delete --> G[删除元素];
E -- replace --> H[替换元素文本];
E -- print_document --> I[打印整个文档];
E -- 其他 --> J[抛出错误];
通过对 Ruby 内部和外部 DSL 的深入了解,开发者可以更好地利用这两种技术,提高开发效率,满足不同用户的需求。无论是处理 XML 数据,还是进行测试用例编写、任务管理等,DSL 都能发挥重要的作用。
Ruby内外DSL深度解析
超级会员免费看
6

被折叠的 条评论
为什么被折叠?



