Programming Ruby

The Pragmatic Programmer's Guide

上一章< 目录 ^
下一章>

模块 Modules



模块是一种集中方法,类和常量的方法,主要带来两个好处:
  1. 模块提供了一个命名空间(namespace )防止命名冲突。
  2. 通过模块能实现混合插入(mixin)功能。

命名空间

当你写的Ruby程序越来越大,越来越多之后,都会发现有很多代码都可以重用,通常可以将相关例程组成一个库,分布到不同的文件以便被其它Ruby程序共享。

通常,这些代码都以类的形式组织在一起,所以你可能将一个类或者和其相关的几个类放到一个文件中。

但是,有时候你也许需要把一些不能在常理上认为属于一个类的东西组合到一起。

最初的办法可能是将所有这些东西都放到一个文件中,然后在需要使用它的程序中引入这个文件,就像C语言那样。但是,这样做有一个问题,比如你编写了一系列的三角函数sin,cos等,你将它们放到一个文件中,比如trig.rb,同时,Sally也做着类似的工作,创建了自己的库文件action.rb,包括自己的一些例程,其中有beGood和sin方法。Joe想编写一个程序,需要同时用到trig.rb和action.rb两个文件,但是这两个文件都定义了一个方法sin,这可不是什么好消息。

答案是模块机制。模块定义了一个命名空间,在这个空间里,你的方法和常量可以不必担心和别人的重名,比如三角函数(trig)就可以放到一个模块中:

module Trig
  PI = 3.141592654
  def Trig.sin(x)
   # ..
  end
  def Trig.cos(x)
   # ..
  end
end

然后Sally的方法可以放到另一个模块:

 

module Action
  VERY_BAD = 0
  BAD      = 1
  def Action.sin(badness)
    # ...
  end
end

模块中常量的命名和类中的一样,以大写字母开头。方法定义也类似,模块的方法定义和类方法定义类似,格式为mod_name.method_name。

如果第三个程序需要使用这些模块,只需要简单的把这些模块载入(使用Ruby的require语句,将在103页讨论),然后引用 限定的名字( qualified names)。

require "trig"
require "action"

 
y = Trig.sin(Trig::PI/4) 
wrongdoing = Action.sin(Action::VERY_BAD)
  

和类方法一样,调用一个模块方法也以这个模块名字为前缀然后一个点再加上方法名;引用一个模块的则是在模块后面加两个冒号再加常量。

 

混合插入(Mixins

模块还有一个非常有用的作用,即通过模块使用叫做混合插入(mixin)的机制实现了多重继承。

在上一节的例子里,我们定义了模块方法,方法名前面加上了模块名作为前缀。如果这样让你想到这是类方法,那么你可能会进一步想“如果我在模块里面定义实例变量会怎样呢?”这个问题非常好,一个模块不能产生实例,因为它不是类。但是,你可以在一个类的定义里包含(include )一个模块,这时候,这个模块中所有的实例方法都变成了在这个类所拥有(能使用)的方法了(all the module's instance methods are suddenly available as methods in the class as well)。这就叫做mixin,实际上,mix-in很像超类(superclasses)。

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模块,PhonographEightTrack 都能访问whoAmI?实例方法。

关于include语句需要注意几点。首先,这个语句和文件无关,C程序员也使用预处理指令#include来将一个文件的内容在编译的时候加入到另一个文件。而Ruby的include语句只是创建了一个指向一个有名字的模块的引用,如果这个模块在一个独立的文件中,那么你必须先用require将这个文件引入,然后才能使用include。第二,Ruby的include不是简单的将模块的实例方法拷贝到类里面,而是建立一个从类到模块的引用。如果很多个类都包含了同一个模块,它们都指向同一样东西。如果你修改了模块中一个方法的定义,即使你的程序还在运行之中,你的类也能使用新的方法的行为[ 注意,我们这里说的是实例方法,实例变量永远都是每个对象都有一份拷贝]

Mixin使得你能非常方便的给类增加方法,它的真正的强大之处在于能使模块中的代码和引入它的类中的代码能相互作用。我们以标准的一个Ruby模块Comparable 为例来说明,这个模块可以用来给类增加比较操作符(<,<= ,==,>=,>等)和between?方法。为了使得它能工作,Comparable 假设使用它的类都定义了 <=>操作符,所以作为类的创建者,你定一个<=>方法,,引入Comparable模块,然后,你就免费得到了6个其它的方法。在我们的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方法,我们可以把它改得更通用一些,通过使用一个能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的例子,可以参看403页开始的关于Enumerable 模块的文档。

 

Mixins中的实例变量(Instance Variables

从C++转到Ruby来的人经常问我们“在C++中,我们必须绕一些弯才能控制如何在多重继承中共享实例变量,mixin中的实例变量会怎么样呢?Ruby怎么处理这种情况呢?”

这对于初学者来说不是什么大问题,记住Ruby中的实例变量是如何工作的:以"@"作为前缀的变量作为当前对象self的实例变量。

对于混合插入来说,你要引入的模块可以在你的类里创建实例变量,并且可以用attr和friends来定义这些变量的访问器(accessor )。比如:

(For a mixin, this means that the module that you mix into your client class (the mixee?) may create instance variables in the client object and may use attr and friends to define accessors for these instance variables. ) 

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

我们不仅访问了模块中的方法,我们也可以访问它的实例变量。但是,这样也会有一定的风险,比如不同的模块可能定义了一个同名的实例变量,从而产生了冲突。

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的模块本身并不怎么携带实例数据,它们使用访问器(accessors )来从对象取得数据。但是如果你想要创建一个必须要有自己状态的模块,请确定这个模块的实例变量的名字要唯一,不要和其它模块的重名(比如用模块名作为变量名的一部分)

迭代器和模块 Enumerable 

你应该早就注意到了Ruby的集合类(collection classes)支持很多对它的处理:遍历,排序等等,没准你也会想要是自己的类支持这么多优秀的特点就更好了。

当然,答案是肯定的,通过使用混合插入这个有用的机制和模块Enumerable,你只需要再写一个名为each的迭代方法就可以了,在这个方法里,依次返回你自己的集合类的元素。混合插入Enumerable模块,然后你的类就支持了比如map,include?,find_all?等方法了。如果你的集合里的元素对象支持<=>方法,那么你的这个集合也可以得到min,max,sort方法。

 

包含(Including)其它文件

因为使用Ruby轻松的能编写优良的模块化的代码,你会经常发现自己会写一些包含自包含的代码,比如面向x的接口,用于y的算法等。一般的时候,我们会把这些文件作为类或者模块的库。

有了这些文件,如果你想把它们整合到你的新程序中,Ruby提供了两种方法:

load "filename.rb"

 
require "filename"
  

load方法每次执行都会包含一个Ruby源文件,而require只会包含一个文件一次,而且,require还有别的功能,它还能装载共享二进制的库文件( load shared binary libraries)。这两个方法都接收相对和绝对的文件路径作为参数,如果给的是相对路径(或者只是一个文件名),系统将会在当前的装载路径(load path)中的每个文件夹下寻找这个文件(装载路径保存在全局变量$:中,见140页的讨论)

使用load和require包含的文件也可以包含其它文件。其中需要注意的是require是一个可执行的语句,它可以在一个if语句里使用,or it may include a string that was just built。包含时候的查找路径也可以在运行时候更改,你可以将需要的目录加到$:,这是一个数组。

因为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


上一章 < 目录 ^
下一章 >

Extracted from the book "Programming Ruby - The Pragmatic Programmer's Guide"
Copyright © 2001 by Addison Wesley Longman, Inc. This material may be distributed only subject to the terms and conditions set forth in the Open Publication License, v1.0 or later (the latest version is presently available at http://www.opencontent.org/openpub/)).

Distribution of substantively modified versions of this document is prohibited without the explicit permission of the copyright holder.

Distribution of the work or derivative of the work in any standard (paper) book form is prohibited unless prior permission is obtained from the copyright holder.