模块是一种将方法,类和常量组织在一起的方式。模块能够提供两个主要的好处:
- 模块提供命名空间防止名字冲突
- 模块实现了 mixin 功能
命名空间
当你开始编写越来越庞大的 Ruby
程序时,你会很自然地找到自己曾经写过可重用的代码,也就是与一般可应用的例程关联的库。你希望把代码分别存储到不同的文件中,这样方便在不同的
Ruby 程序中使用。
通常这些代码会分布在类中,所以你常常将一个类(或一组相关联的类)存储为一个文件。
不过有时你也想要将不能自然组成类的东西组合起来。
一种最原始的方式就是把所有的东西都放入一个文件中,然后在需要的时候加载整个文件。这是
C
语言的工作方式。但这不是一个程序。比方说你写了些三角函数 sin
和
cos
等等。你把它们都装进 trig.rb
文件方便以后使用。同时
Sally
正编写一个善恶模拟平台,也编写了一些自己需要用到的函数,有 beGood
和
sin
,并且把它们都放到 action.rb
文件中。Joe
也想写一个程序,这个程序能够计算有多少天使能够在大头针上跳舞,他需要将
trig.rb
和 action.rb
都加载到自己的程序中。不过有个坏消息是都定义了一个叫做 sin
的方法。
解决的方案就是模块机制。模块定义了一个命名空间,是一个只有你的方法和常量运行而不需要担心进入其他方法和常量的沙盒。我们会将三角函数放入一个模块中:
module Trig
PI = 3.141592654
def Trig.sin(x)
# ..
end
def Trig.cos(x)
# ..
end
end
关于善恶行为的方法就可以放入另一个模块中:
module Action
VERY_BAD = 0
BAD = 1
def Action.sin(badness)
# ...
end
end
模块常量的命名和类常量的一样,都以大写字母开头。模块中方法的定义和类中方法的定义也是相似的。
如果第三方程序想使用这些模块,它需要先加载(可以使用 Ruby 的 require
声明,我们会在 103 页进行讨论)两个文件并引用有效的名字。
require "trig"
require "action"
y = Trig.sin(Trig::PI/4)
wrongdoing = Action.sin(Action::VERY_BAD)
就像类方法一样,你可以通过在方法名前面加上模块名和句点号的方式进行调用,如果是引用常量的话可以用模块名加两个冒号。
Mixins
模块还有其他非常有用的功能。模块可以极大消除大家对多继承的需要,这是因为它提供了一个叫
mixin
的功能。
在前面部分的例子中,我们定义了模块方法并以模块名作为方法名的前缀。如果这些让你想起了类方法,那你接下来会出现另一个想法「如果在模块中定义实例方法会发生什么?」,这是个好问题。模块不会有实例,因此它并不是类。不过你可以在类定义中包含模块。当类中包含模块时,所有的模块实例方法也将作为类中的方法使用。它们融合在了一起。实际上,模块融合是如同超类一样的有效行为。
module Debug
def whoAmI?
"#{self.type.name} (\##{self.id}): #{self.to_s}"
end
end
class Phonograph
include Debug
# ...
end
class EightTrack
include Debug
# ...
end
ph = Phonograph.new("West End Blues")
et = EightTrack.new("Surrealistic Pillow")
ph.whoAmI? »"Phonograph (#537766170): West End Blues"
et.whoAmI? »"EightTrack (#537765860): Surrealistic Pillow"
通过引入 Debug
模块,Phonograph
和 EightTrack
便可以访问
whoAmI?
实例方法。
在我们往下讲之前需要再讲讲关于 include
的几个点。首先,它不会对文件做任何操作。C
语言程序员通过预处理器直接调用 #include
在编译期间添加了另一个文件的内容进来。Ruby 的 include
声明只是单纯对对应名字的模块建立了引用。如果模块是在不同的文件中,你需要在
include
之前使用 require
将文件引入。第二点是 Ruby 的 include
并不是将模块中的实例方法拷贝到类中。它只是在类和模块间建立了引用的关系。如果你将模块中的方法定义修改过,当程序运行起来时,包含这些模块的所有类都将展示新的行为。(当然我们只是在谈论方法。像实例变量还是对象拥有的)。
Mixins 给了你一个将函数添加至类的完美方式。但是当 mixin
中的代码开始与使用它的类中的方法互动时才会显现真正的威力。让我们将 Ruby
中一个标准的 mixin Comparable
作为例子。Comparable
mixin
通常用来添加比较操作,就如同类中的 between?
方法。为了好说明问题,Comparable
假设每个类都是使用它定义的
<=>
操作符。因此作为类的开发者,你引入 Comparable
并且定义了
<=>
方法,你将自然得到比较方法。让我们在 Song
中尝试一下基于歌曲时长的比较。我们只是需要将 Comparable
引入并实现
<=>
操作。
class Song
include Comparable
def <=>(other)
self.duration <=> other.duration
end
end
我们可以通过几首歌曲检测一下。
song1 = Song.new("My Way", "Sinatra", 225)
song2 = Song.new("Bicylops", "Fleck", 260)
song1 <=> song2 »-1
song1 < song2 »true
song1 == song1 »true
song1 > song2 »false
最后回到 43 页我们展示 Smalltalk 中 inject
函数实现的地方,我们是通过 Array
类实现的。我们曾经许诺过它还有更简洁的实现方式。难道还有比 mixin
模块更好的方式吗?
module Inject
def inject(n)
each do |value|
n = yield(n, value)
end
n
end
def sum(initial = 0)
inject(initial) { |n, value| n + value }
end
def product(initial = 1)
inject(initial) { |n, value| n * value }
end
end
我们也可以通过预设的类测试一下。
class Array
include Inject
end
[ 1, 2, 3, 4, 5 ].sum »15
[ 1, 2, 3, 4, 5 ].product »120
class Range
include Inject
end
(1..5).sum »15
(1..5).product »120
('a'..'m').sum("Letters: ") »"Letters: abcdefghijklm"
如果还想了解更多 mixin 的例子,可以阅读 Enumerable
模块的文档,文档从 403 页开始。
Mixins 中的实例变量
从 C++ 语言转向 Ruby 的人常问我们「如果 mixin
中有实例变量会怎样?在 C++
中,我需要跳过一些障碍才可以控制变量在多继承体系中的共享。Ruby
是怎么做的呢?」
对于初学者来说这并不是一个公平的问题。请记住实例变量在 Ruby
中是怎样运转的,首先得提到 @
前缀的变量在当前对象中创建了实例变量。
对于 mixin
来说,这意味着被融合到类中的模块可以在这个类中创建实例变量,并且可以用
attr
定义实例变量的访问器。比如:
module Notes
attr :concertA
def tuning(amt)
@concertA = 440.0 + amt
end
end
class Trumpet
include Notes
def initialize(tune)
tuning(tune)
puts "Instance method returns #{concertA}"
puts "Instance variable is #{@concertA}"
end
end
# The piano is a little flat, so we'll match it
Trumpet.new(-5.3)'
结果是:
Instance method returns 434.7
Instance variable is 434.7
我们不仅可以访问 mixin
中定义的方法,也可以访问必要的实例变量。不过这是一个风险,不同的 mixins
可能使用相同名字的实例变量而导致冲突:
module MajorScales
def majorNum
@numNotes = 7 if @numNotes.nil?
@numNotes # Return 7
end
end
module PentatonicScales
def pentaNum
@numNotes = 5 if @numNotes.nil?
@numNotes # Return 5?
end
end
class ScaleDemo
include MajorScales
include PentatonicScales
def initialize
puts majorNum # Should be 7
puts pentaNum # Should be 5
end
end
ScaleDemo.new
结果是:
7
7
有两段代码我们都使用了 @numNotes
实例变量。但结果可能与编写者期待的不一样。
最大的一个问题是 mixin
本身并不携带自己的实例数据,它们都是使用从客户对象处获取的数据。如果你需要创建一个拥有自己状态的
mixin,并且确保实例变量有一个与同一系统中其他 mixin
变量不同的名字(或许可以将模块的名字作为变量名字的一部分来完成)。
迭代器和可枚举模块
你可能已经注意到 Ruby
中的集合支持许多操作,包括遍历,排序等等。你可能会想「Gee
我也希望自定义的类能够拥有这些优雅的功能」。(如果你确实这样想过,也是时候停止观看六十年代的电视节目了)。
自定义的类可以支持这些功能都需要感谢 mixin 和 Enumerable
模块的魔法。不过你必须编写 each
迭代器,它将依次返回你集合中的元素。融合了 Enumerable
后,你的类将支持 map
,include?
和 find_all?
方法。如果自定义集合中的对象通过 <=>
实现了有用的排序,自定义集合也会支持 min
,max
和 sort
。
包含其他文件
由于 Ruby
的模块化代码使得可以更好地编写代码,所以你常常会发现自己会写一些拥有独立功能的小代码模块,比如关于
x 的接口,一个完成 y
功能的算法等等。一般情况下,你都会把这些文件组织成类或者模块库。
有一些写好的代码文件,你想在新程序中引用它们。Ruby
提供了两个声明帮助你:
load "filename.rb"
require "filename"
load
方法在每次执行时都会加载对应名字的 Ruby 源文件,而 require
只加载一次指定文件。require
还有额外功能,它可以加载二进制库。例程对于相对路径和绝对路径都可以访问。如果是相对路径(或者只是一个文件名),它们会查找当前加载路径($:,在 140 页讨论过)下的每个文件夹。
文件可以通过 load
和 require
加载,这些文件中也可以包含其他文件等等。require
是一个可执行的声明这点可能并不明显,它也可以参与 if
声明,或者包含创建的字符串。查找路径可能在运行时发生改变。只要将你想切换的文件夹路径给
$:
即可。
因为 load
会无条件引入源文件,所以你可以用它加载在程序启动后会发生变化的源文件:
5.times do |i|
File.open("temp.rb","w") { |f|
f.puts "module Temp\ndef Temp.var() #{i}; end\nend"
}
load "temp.rb"
puts Temp.var
end
结果是:
0
1
2
3
4
本文翻译自《Programming Ruby》,主要目的是自己学习使用,文中翻译不到位之处烦请指正,如需转载请注明出处
本章原文为
Modules
网友评论