Ruby 元编程:创建可自我修改和修改子类的类
1. 加密字符串方法与单元测试
在编程中,我们常常会遇到加密字符串的需求。以下是一个简单的加密字符串的方法:
def encrypt_string( string )
string.tr( 'a-zA-Z', 'm-za-lM-ZA-L')
end
这个方法通过字符替换的方式对字符串进行加密。不过,在代码中还存在一个有趣的情况,当尝试关闭加密功能时,会定义一个名为
incrypt_string
的方法,但这个方法只是简单返回原字符串:
def incrypt_string( string )
string
end
这里体现了一个重要的原则,对于代码,尤其是元编程代码,单元测试是必不可少的。以下是使用 RSpec 进行的单元测试示例:
describe Document do
before :each do
@doc = Document.new( "test", "tester", "this is a test" )
end
it "should encrypt if encryption is enabled" do
Document.enable_encryption( true )
@doc.encrypt_string( 'abc' ).should_not == 'abc'
end
it "should not encrypt if encryption is disabled" do
Document.enable_encryption( false )
@doc.encrypt_string( 'abc' ).should == 'abc'
end
end
这个测试确保了加密功能在开启和关闭时的正确性。
2. Ruby 类的可执行性
Ruby 类具有可执行性,在定义类的过程中,Ruby 会执行类定义中的代码,并对类进行修改。这意味着我们可以在类定义中插入逻辑,从而决定类的具体形态。例如,我们可以根据不同的条件来定义类的方法。
3. 更真实的文档类
之前我们的
Document
类过于简单,无法真实地模拟现实世界中的文档。现实中的文档包含段落和字体等信息,因此我们可以创建一个更真实的文档类。首先,定义一个段落类:
class Paragraph
attr_accessor :font_name, :font_size, :font_emphasis
attr_accessor :text
def initialize( font_name, font_size, font_emphasis, text='')
@font_name = font_name
@font_size = font_size
@font_emphasis = font_emphasis
@text = text
end
def to_s
@text
end
# Rest of the class omitted...
end
然后,定义一个结构化文档类:
class StructuredDocument
attr_accessor :title, :author, :paragraphs
def initialize( title, author )
@title = title
@author = author
@paragraphs = []
yield( self ) if block_given?
end
def <<( paragraph )
@paragraphs << paragraph
end
def content
@paragraphs.inject('') { |text, para| "#{text}\n#{para}" }
end
# ...
end
使用这些类,我们可以创建一个简历文档:
russ_cv = StructuredDocument.new( 'Resume', 'RO' ) do |cv|
cv << Paragraph.new( :nimbus, 14, :bold, 'Russ Olsen' )
cv << Paragraph.new( :nimbus, 12, :italic, '1313 Mocking Bird Lane')
cv << Paragraph.new( :nimbus, 12, :none, 'russ@russolsen.com')
# .. and so on
end
4. 子类化的问题
虽然我们可以创建子类来提供一些有用的方法,例如为简历和安装说明创建子类:
class Resume < StructuredDocument
def name( text )
paragraph = Paragraph.new( :nimbus, 14, :bold, text )
self << paragraph
end
def address( text )
paragraph = Paragraph.new( :nimbus, 12, :italic, text )
self << paragraph
end
def email( text )
paragraph = Paragraph.new( :nimbus, 12, :none, text )
self << paragraph
end
# and so on
end
class Instructions < StructuredDocument
def introduction( text )
paragraph = Paragraph.new( :mono, 14, :none, text )
self << paragraph
end
def warning( text )
paragraph = Paragraph.new( :arial, 22, :bold, text )
self << paragraph
end
def step( text )
paragraph = Paragraph.new( :nimbus, 14, :none, text )
self << paragraph
end
# and so on
end
但是,这些子类中的方法存在大量的重复代码。每个辅助方法的结构都非常相似,只是字体名称和大小等参数不同。
5. 类方法创建实例方法
为了避免这些重复代码,我们可以利用 Ruby 的元编程特性。具体来说,我们可以创建一个类方法来动态地创建实例方法。例如,我们希望能够这样定义一个类:
class Instructions < StructuredDocument
paragraph_type( :introduction,
:font_name => :arial,
:font_size => 18,
:font_emphasis => :italic )
# And so on...
end
为了实现
paragraph_type
方法,我们需要将其定义为
StructuredDocument
类的类方法:
class StructuredDocument
def self.paragraph_type( paragraph_name, options )
# What do we do in here?
end
# ...
end
这个方法接受两个参数:新段落类型的名称(也是要添加到类中的方法名称)和一个包含字体信息的选项哈希。
6. 使用
class_eval
创建方法
在
paragraph_type
方法内部,我们不能直接使用
def
语句来定义方法,因为
def
需要一个明确的方法名,而我们的方法名是作为参数传递的。因此,我们可以使用
class_eval
方法来解决这个问题:
class StructuredDocument
def self.paragraph_type( paragraph_name, options )
name = options[:font_name] || :arial
size = options[:font_size] || 12
emphasis = options[:font_emphasis] || :normal
code = %Q{
def #{paragraph_name}(text)
p = Paragraph.new(:#{name}, #{size}, :#{emphasis}, text)
self << p
end
}
class_eval( code )
end
# ...
end
例如,当我们调用
paragraph_type( :introduction, :font_name => :arial, :font_size => 18, :font_emphasis => :italic )
时,
paragraph_type
方法会创建一个包含新方法代码的字符串,然后使用
class_eval
执行该字符串,从而创建一个名为
introduction
的实例方法。
7. 使用
define_method
更好地创建方法
虽然
class_eval
可以实现动态创建方法的功能,但它并不是最理想的方式。我们可以使用
define_method
来更方便地创建方法:
class StructuredDocument
def self.paragraph_type( paragraph_name, options )
name = options[:font_name] || :arial
size = options[:font_size] || 12
emphasis = options[:font_emphasis] || :none
define_method(paragraph_name) do |text|
paragraph = Paragraph.new( name, size, emphasis, text )
self << paragraph
end
end
# ...
end
使用
define_method
可以避免使用字符串拼接,使代码更加清晰和易于维护。
8. 对实例方法的修改
除了创建实例方法,我们还可以对实例方法进行其他修改。例如,我们可以定义一个
privatize
方法来将
content
方法设置为私有:
class StructuredDocument
# Rest of the class omitted...
def self.privatize
private :content
end
end
然后在子类中调用这个方法:
class BankStatement < StructuredDocument
paragraph_type( :bad_news,
:font_name => :arial,
:font_size => 60,
:font_emphasis => :bold )
privatize
end
这样,当我们尝试访问
BankStatement
实例的
content
方法时,会得到一个错误,因为该方法已经被设置为私有。
9. 类方法的修改
我们还可以修改类方法。例如,我们可以定义一个
disclaimer
方法来声明文档的可用性:
class StructuredDocument
# Rest of the class omitted...
def self.disclaimer
"This document is here for all to see"
end
def self.privatize
private :content
def self.disclaimer
"This document is a deep, dark secret"
end
end
end
如果在子类中调用
privatize
方法,子类将拥有自己的
disclaimer
方法:
class BankStatement < StructuredDocument
paragraph_type( :bad_news,
:font_name => :arial,
:font_size => 60,
:font_emphasis => :bold )
privatize
end
此时,调用
BankStatement.disclaimer
将返回
"This document is a deep, dark secret"
。
10. 现实世界中的子类修改方法
在现实世界的 Ruby 代码中,有很多子类修改方法的例子。例如,
attr_accessor
、
attr_reader
和
attr_writer
就是内置的子类修改方法。以下是一个简单的
attr_reader
实现:
class Object
def self.simple_attr_reader(name)
code = "def #{name}; @#{name}; end"
class_eval( code )
end
end
以及一个简单的
attr_writer
实现:
class Object
def self.simple_attr_writer(name)
method_name = "#{name}="
define_method( method_name ) do |value|
variable_name = "@#{name}"
instance_variable_set( variable_name, value )
end
end
end
另外,ActiveRecord 中的
has_one
等方法也是子类修改方法的例子。还有
forwardable.rb
模块,它可以动态地生成委托方法。例如:
class DocumentWrapper
extend Forwardable
def_delegators :@real_doc, :title, :author, :content
def initialize( real_doc )
@real_doc = real_doc
end
end
通过这些方法,我们可以将
DocumentWrapper
实例的方法调用委托给
@real_doc
实例。
11. 避免陷入困境
在使用元编程技术时,很容易迷失方向。以下是一些建议,可以帮助我们避免陷入困境:
-
明确目标
:在我们的例子中,目标是方便地为
StructuredDocument
子类添加段落生成方法。
-
了解执行时机
:当加载
StructuredDocument
代码时,会得到一个通用的
StructuredDocument
类,其中包含
paragraph_type
类方法。当加载子类(如
Instructions
类)时,会调用
paragraph_type
方法,从而为子类添加新的方法。只有在所有定义完成后,才能创建子类的实例并调用生成的方法。
-
理解
self
的值
:在
StructuredDocument
类的
paragraph_type
方法中,
self
是调用该方法的子类(如
Resume
或
Instructions
),而不是
StructuredDocument
。因此,新创建的方法会添加到子类中。当调用生成的方法时,
self
是子类的实例。
-
合理使用元编程
:在编程时,我们需要根据问题的复杂程度来决定是否使用元编程。如果可以用传统的编程方法合理地解决问题,就应该优先使用传统方法。例如,如果需要在多个类之间共享一个方法,将该方法放在超类或混合模块中可能是更好的选择。但对于一些无法用传统方法解决的问题,如构建通用的方法无关代理或可重新加载的
Document
类,元编程是唯一的解决方案。对于一些中间情况,如“是否加密”的问题和 Ruby 1.8/1.9 兼容性处理,需要根据具体情况进行权衡。
总之,Ruby 的元编程特性为我们提供了强大的工具,可以帮助我们避免重复代码,提高编程效率。但在使用时,我们需要谨慎考虑,确保代码的可读性和可维护性。
以下是一个简单的流程图,展示了使用
paragraph_type
方法创建实例方法的过程:
graph TD;
A[定义 StructuredDocument 类] --> B[定义 paragraph_type 类方法];
B --> C[接收段落名称和选项哈希];
C --> D[提取字体信息];
D --> E[生成方法代码字符串];
E --> F[使用 class_eval 执行代码字符串];
F --> G[在子类中创建实例方法];
另外,我们可以用表格总结一下不同方法的特点:
| 方法 | 优点 | 缺点 |
| – | – | – |
|
class_eval
| 可以动态创建方法,代码直观 | 需要处理字符串拼接,容易出错 |
|
define_method
| 代码简洁,避免字符串拼接 | 对于初学者可能不太容易理解 |
通过这些技术和方法,我们可以更好地利用 Ruby 的元编程特性,创建出更加灵活和可维护的代码。
Ruby 元编程:创建可自我修改和修改子类的类
12. 元编程技术的综合应用示例
为了更深入地理解元编程技术在实际中的应用,我们来看一个综合示例。假设我们要创建一个
Report
类,它是
StructuredDocument
的子类,并且需要根据不同的报告类型动态生成一些特定的段落方法。
class Report < StructuredDocument
paragraph_type( :summary,
:font_name => :times,
:font_size => 14,
:font_emphasis => :normal )
paragraph_type( :conclusion,
:font_name => :times,
:font_size => 14,
:font_emphasis => :bold )
end
现在,我们可以使用这些动态生成的方法来创建一个报告实例:
sales_report = Report.new( 'Sales Report', 'Sales Team' ) do |report|
report.summary( 'The sales this quarter have increased by 20%.' )
report.conclusion( 'We should continue with the current marketing strategy.' )
end
这个示例展示了如何利用元编程技术快速创建具有特定功能的子类,避免了大量重复的代码编写。
13. 元编程与代码复用
元编程的一个重要优势是提高代码复用性。通过在超类中定义通用的类方法来创建实例方法,我们可以在多个子类中复用这些逻辑。例如,我们可以将
paragraph_type
方法进一步扩展,使其支持更多的自定义选项:
class StructuredDocument
def self.paragraph_type( paragraph_name, options )
name = options[:font_name] || :arial
size = options[:font_size] || 12
emphasis = options[:font_emphasis] || :none
prefix = options[:prefix] || ''
suffix = options[:suffix] || ''
define_method(paragraph_name) do |text|
full_text = "#{prefix}#{text}#{suffix}"
paragraph = Paragraph.new( name, size, emphasis, full_text )
self << paragraph
end
end
end
现在,我们可以在子类中使用更多的自定义选项:
class SpecialReport < StructuredDocument
paragraph_type( :highlight,
:font_name => :helvetica,
:font_size => 16,
:font_emphasis => :italic,
:prefix => '>> ',
:suffix => ' <<' )
end
special_report = SpecialReport.new( 'Special Report', 'Research Team' ) do |report|
report.highlight( 'This is an important finding.' )
end
这样,我们可以在不同的子类中灵活地复用
paragraph_type
方法,根据具体需求进行定制。
14. 元编程的性能考虑
虽然元编程可以带来很多便利,但也需要考虑其性能影响。动态创建方法和修改类的操作可能会消耗一定的时间和资源,尤其是在频繁调用这些操作时。例如,使用
class_eval
或
define_method
动态创建方法时,每次调用都会进行一定的解析和执行操作。
为了减少性能开销,我们可以采取以下措施:
-
缓存生成的方法
:如果某些方法的生成逻辑是固定的,可以考虑在第一次生成后进行缓存,避免重复生成。
-
延迟加载
:将一些元编程操作延迟到真正需要时再执行,而不是在类定义时就进行大量的动态修改。
15. 元编程与代码调试
元编程代码的调试可能会比传统代码更具挑战性,因为动态生成的方法和修改的类可能会使代码的执行流程变得复杂。以下是一些调试元编程代码的建议:
-
打印调试信息
:在关键的元编程操作中添加打印语句,输出关键变量的值和执行步骤,帮助我们理解代码的执行过程。
-
使用调试工具
:利用 Ruby 的调试工具(如
pry
)来逐步执行代码,查看变量的值和方法的调用情况。
-
单元测试
:编写详细的单元测试,确保元编程代码的正确性。通过测试用例覆盖不同的情况,验证生成的方法是否按预期工作。
16. 元编程的未来发展趋势
随着 Ruby 语言的不断发展,元编程技术也可能会有新的发展和应用。以下是一些可能的发展趋势:
-
更强大的元编程 API
:Ruby 可能会提供更多的元编程 API,使开发者能够更方便地进行类和方法的修改。
-
与其他技术的结合
:元编程可能会与其他技术(如机器学习、大数据)结合,创造出更强大的应用。
-
提高性能和安全性
:未来的元编程技术可能会在性能和安全性方面进行优化,减少性能开销并提供更安全的编程环境。
17. 总结
通过本文的介绍,我们了解了 Ruby 元编程中创建可自我修改和修改子类的类的相关技术。从加密字符串方法的单元测试,到创建结构化文档类和子类,再到利用元编程技术避免重复代码,我们看到了元编程的强大威力。
在使用元编程时,我们需要注意以下几点:
- 明确目标,合理使用元编程技术,避免过度使用或滥用。
- 了解代码的执行时机和
self
的值,确保动态生成的方法和修改的类按预期工作。
- 考虑性能和调试问题,采取相应的措施来提高代码的性能和可维护性。
总之,元编程是 Ruby 语言的一大特色,它为我们提供了一种强大的编程方式,但也需要我们谨慎使用,以充分发挥其优势。
以下是一个流程图,展示了元编程代码的开发和调试过程:
graph TD;
A[确定需求] --> B[设计元编程方案];
B --> C[编写元编程代码];
C --> D[添加调试信息];
D --> E[进行单元测试];
E --> F{测试是否通过};
F -- 是 --> G[优化性能];
F -- 否 --> H[调试代码];
H --> C;
G --> I[部署和维护];
另外,我们可以用表格总结一下元编程的优缺点:
| 优点 | 缺点 |
| – | – |
| 避免重复代码,提高编程效率 | 可能会降低代码的可读性和可维护性 |
| 可以动态创建方法和修改类,增加代码的灵活性 | 性能开销较大,需要谨慎使用 |
| 能够解决一些传统编程方法难以解决的问题 | 调试难度较大 |
通过合理利用元编程技术,我们可以在 Ruby 编程中创造出更加灵活、高效和可维护的代码。
超级会员免费看
4

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



