介绍一种跟类相似的构造:模块(module)。在设计程序的时候,我们会把大的组件分割成小块,你可以混合与匹配对象的行为。
跟类差不多,模块也捆绑方法与常量。不一样的是,模块没有实例。你可以把拥有特定功能的模块放到类或某个特定的对象里使用。
Class 这个类是 Module 类的一个子类,也就是所有的 class 对象应该也是一个 module 对象。
上午10:26 ***
创建与使用模块
上午10:26 ***
module MyFirstModule
def say_hello
puts 'hello'
end
end
我们创建了类以后可以去创建这个类的实例,实例可以执行类里面的实例方法。不过模块是没有实例的,模块可以混合(mixed in,mix-in,mixin)到类里面,用的方法是 include 还有 prepend 。这样类的实例就可以使用在模块里面定义的实例方法了。
使用一下上面定义的那个模块:
class ModuleTester
include MyFirstModule
end
mt = ModuleTester.new
mt.say_hello
上面的 ModuleTester 对象调用了 say_hello 这个方法,这样会输出一个 hello 。这个方法是混合到 ModuleTester 类里面的 MyFirstModule 里定义的实例方法。
在类里混合使用模块很像是去继承一个 superclass 。比如 B 类继承了 A 类,这样 B 类的实例就可以调用来自 A 类的实例方法。再比如 C 类混合了模块 M,这样 C 类的实例就可以调用在模块 M 里定义的方法。继承类与混合模块的区别是,你可以在一个类里混合使用多个模块,你不能让一个类去继承多个类。
模块可以让我们在多个类之间共用它的代码,因为任何的类都可以混合使用同一个模块。
创建一个模块
模块给我们提供了收集与封装行为的方法。下面我们可以写一个模块,去封装一些像堆(stack)的特性,然后把模块混合到一个或多个类里面,这样模块里的行为就会传授给对象。
堆(stack)是一种数据格式,后进来的,先出去(LIFO:last in, first out)。比如一堆盘子,用的第一个盘子,是最后一次放到这堆里的那个。经常跟堆一起讨论的还有个概念:队列(queue),它是先进来的,先出去(FIFO),比如在民政局窗口前排的队,排在第一位置上的人最先办完手续。
先把下面代码放到 stacklike.rb 文件里:
module Stacklike
def stack
@stack ||= []
end
def add_to_stack(obj)
stack.push(obj)
end
def take_from_stack
stack.pop
end
end
在上面的 Stacklike 模块里,我们使用了一个数组来表示堆,这个数组会存储在一个实例变量里面,名字是 @stack,这个实例变量可以通过 stack 这个方法得到。这个方法使用了条件设置变量,||= 是一个操作符,只有变量不是 nil 或 false 的时候,才会让这个变量的值等于一个特定的值。这里就是第一次调用 stack 的时候,它会设置 @stack 让它等于一个空白的数组,后续再次调用的时候,@stack 已经有值了,也就会去返回它的值。
调用 add_to_stack 方法会把一个对象添加到堆里面,就是会把对象添加到 @stack 数组的最后。take_from_stack 会删除掉数组里的最后一个对象。这些方法里用的 push 还有 pop ,它们是 Array 类里的实例方法。
我们定义的这个 Stacklike 模块,其实就是有选择的实施了已经在 Array 对象里存在的一些行为,添加一个元素到数组的最后,删除数组里的最后一个元素。相比堆,数组更灵活一些,堆不能干所有数组能干的事。比如你可以删除掉数组里的任意顺序的项目,在堆里就不行,你只能删除掉最近添加进来的元素。
现在我们定义好了一个模块,它实施了堆的一些行为,也就是管理一些项目,新的项目可以添加到最后,最近添加进来的可以被删除掉。下面再看一下怎么样使用模块。
在类里混合模块
做个实验,创建一个文件,名字是 stack.rb,添加下面这段代码:
require_relative 'stacklike'
class Stack
include Stacklike
end
这里混合用的方法是 include ,把 Stacklike 这个模块混合到了 Stack 这个类里,这样 Stack 类的对象就会拥有在 Stacklike 模块里定义的方法了。
使用 require 或 load 的时候,要加载的东西放到了一组引号里面,但是使用 include 与 prepend 的时候加载的东西不需要使用引号。因为 require 与 load 要使用字符串作为它们的参数值,include 载入的是模块的名字,模块的名字是常量。require 与 load 要找到在磁盘上的文件,include 与 prepend 会在内存里操作。
类的名字用的是名词,模块的名字用的是形容词。Stack objects are stacklike 。
做个实验:
s = Stack.new
s.add_to_stack('项目 1')
s.add_to_stack('项目 2')
s.add_to_stack('项目 3')
puts '当前在堆里的对象:'
puts s.stack
taken = s.take_from_stack
puts '删除了对象:'
puts taken
puts '现在堆里是:'
puts s.stack
执行一下会输出:
当前在堆里的对象:
项目 1
项目 2
项目 3
删除了对象:
项目 3
现在堆里是:
项目 1
项目 2
继续使用模块
再做个实验,创建一个文件,名字是 cargohold.rb(飞机货舱),代码如下:
require_relative 'stacklike'
class Suitcase
end
class CargoHold
include Stacklike
def load_and_report(obj)
print 'loading object:'
puts obj.object_id
add_to_stack(obj)
end
def unload
take_from_stack
end
end
ch = CargoHold.new
sc1 = Suitcase.new
sc2 = Suitcase.new
sc3 = Suitcase.new
ch.load_and_report(sc1)
ch.load_and_report(sc2)
ch.load_and_report(sc3)
first_unloaded = ch.unload
print '第一个下飞机的行里是:'
puts first_unloaded.object_id
执行它的结果是:
loading object:70328907390400
loading object:70328907390380
loading object:70328907390360
第一个下飞机的行里是:70328907390360
下午12:00 ***
模块,类与方法查找
下午12:06 ***
对象收到发送给它的信息以后,它会试着去执行跟信息一样的方法,方法可以是对象所属的类里面定义的,或者这个类的 superclass,或者是混合到这个类里的模块提供的。发送信息给对象究竟发生了什么?
方法查找
下面这个例子演示了加载模块与类的继承:
module M
def report
puts "'report' 方法在模块 M 里"
end
end
class C
include M
end
class D < C
end
obj = D.new
obj.report
report 这个实例方法是在模块 M 里定义的,在 C 类里面混合了模块 M ,D 类是 C 类的子类,obj 是 D 类的一个实例,obj 这个对象可以调用 report 方法。
从对象的视角来看一下,假设你就是一个对象,有人给你发了个信息,你得想办法作出回应,想法大概像这样:
我是个 Ruby 对象,别人给我发了个 'report' 信息,我得在我的方法查找路径里,试着去找一个叫 report 的方法,它可能在一个类或者模块里。
我是 D 类的一个实例。D 类里有没有 report 这个方法?
没有
D 类有没有混合使用模块?
没有
D 类的超级类(superclass)C,里面有没有定义 report 这个实例方法?
没有
C 类里混合模块了没?
是的,混合了模块 M
那 M 模块里有没有 report 这个方法?
有
好地,就调用一下这个方法。
找到了这个方法搜索就结束了,没找到就会触发错误,这个错误是用 method_missing 方法触发的。
同名方法
同一个名字的方法在任何时候,在每个类或模块里只能出现一次。一个对象会使用它最先在找到的方法。
做个实验:
module M
def report
puts '在模块 M 中的 report'
end
end
module N
def report
puts '在模块 N 中的 report'
end
end
class C
include M
include N
end
c = C.new
c.report
执行它的结果会是:
在模块 N 中的 report
多次加载同一个模块是无效的,这样试一下:
class C
include M
include N
include M
end
执行的结果仍然会是:
在模块 N 中的 report
下午12:49 ****
prepend
下午1:40 ***
使用 prepend 加载的模块,对象会先使用。也就是如果一个方法在类与模块里都定义了,会使用用了 prepend 加载的模块里的方法。
来看个例子:
module MeFirst
def report
puts '来自模块的问候'
end
end
class Person
prepend MeFirst
def report
puts '来自类的问候'
end
end
p = Person.new
p.report
执行的结果会是:
来自模块的问候
super
做个实验:
module M
def report
puts '在模块 M 里的 report 方法'
end
end
class C
include M
def report
puts 'C 类里的 report 方法'
puts '触发上一级别的 report 方法'
super
puts "从调用 super 那里回来了"
end
end
c = C.new
c.report
执行的结果是:
C 类里的 report 方法
触发上一级别的 report 方法
在模块 M 里的 report 方法
从调用 super 那里回来了
c 是 C 类的一个实例,c.report 是给 c 发送了一个 report 信息,收到以后开始查找方法,先找到的 C 类,这里定义了 report 方法,所以会去执行它。
在 C 类里的 report 方法里,调用了 super,意思就是即使对象找到了跟 report 这个信息对应的方法,它还必须继续查找下一个匹配的 report 方法,下一个匹配是在模块 M 里定义的 report 方法,也就会去执行一下它。
再试一个使用 super 的例子:
class Bicycle
attr_reader :gears, :wheels, :seats
def initialize(gears = 1)
@wheels = 2
@seats = 1
@gears = gears
end
end
class Tandem < Bicycle
def initialize(gears)
super
@seats = 2
end
end
上面有两个类,Bicycle 自行车,Tandem 双人自行车,Tandem 继承了 Bicycle 类。在 Tandem 的 initialize 方法里用了一个 super ,会调用 Bicycle 类里的 initialize 方法,也就是会设置一些属性的默认的值。双人自行车有两个座位,所以我们又重新在 Tandem 的 initialize 方法里设置了一下 @seats 的默认的值。
super 处理参数的行为:
不带参数调用 — super,super 会自动转发参数传递给它调用的方法。
带空白参数的调用 — super(),super 不会发送参数。
带特定参数的调用 — super(a, b, c),super 只会发送这些参数。
下午2:23 ***
method_missing 方法
下午 14:35 ***
Kernel 模块提供了一个实例方法叫 method_missing,如果对象收到一个不知道怎么响应的信息,就会调用这个方法。
试一下:
>> obj = Object.new
=> #<Object:0x007fdef1958fa8>
>> obj.blah
NoMethodError: undefined method `blah' for #<Object:0x007fdef1958fa8>
from (irb):2
from /usr/local/bin/irb:11:in `<main>'
我们可以覆盖 method_missing:
>> def obj.method_missing(m, *args)
>> puts "你不能在这个对象上调用 #{m},试试别的吧。"
>> end
=> :method_missing
>> obj.blah
你不能在这个对象上调用 blah,试试别的吧。
=> nil
组合 method_missing 与 super
一般我们会拦截未知的信息,然后决定到底怎么去处理它,可以处理,也可以把它发送给原来的 method_missing 。使用 super 就很容易实现,看个例子:
class Student
def method_missing(m, *args)
if m.to_s.start_with?('grade_for_')
# return the appropriate grade, based on parsing the method name
else
super
end
end
end
上面的代码,如果调用的方法是用 grade_for 开头的就会被处理,比如 grade_for_english 。如果不是,就会调用原始的 method_missing 。
再试一个复杂点的例子。比如我们要创建一个 Person 类,这个类可以这样用:
j = Person.new("John")
p = Person.new("Paul")
g = Person.new("George")
r = Person.new("Ringo")
j.has_friend(p)
j.has_friend(g)
g.has_friend(p)
r.has_hobby("rings")
Person.all_with_friends(p).each do |person|
puts "#{person.name} is friends with #{p.name}"
end
Person.all_with_hobbies("rings").each do |person|
puts "#{person.name} is into rings"
end
我们想要输出的东西像这样:
John is friends with Paul
George is friends with Paul
Ringo is into rings
一个人可以有朋友和爱好,Person 可以找出某个人的所有的朋友,或者拥有某个爱好的所有的人。这两个功能是用 all_with_friends 还有 all_with_hobbies 这两个方法实现的。
Person 类上的 all_with_* 方法可以使用 method_missing 改造一下,在类里定义一段代码:
class Person
def self.method_missing(m, *args)
# code here
end
end
m 是方法的名字,它可以是用 all_with 开头的,也可以不是,如果是我们就去处理一下它,如果不是就交给原始的 method_missing 。再这样修改一下:
class Person
def self.method_missing(m, *args)
method = m.to_s
if method.start_with?('all_with_')
# 在这里处理请求
else
super
end
end
end
Person 对象要跟踪它所有的朋友与爱好
Person 类跟踪所有的人
每个人都有个名字
class Person
PEOPLE = []
attr_reader :name, :hobbies, :friends
def initialize(name)
@name = name
@hobbies = []
@friends = []
PEOPLE << self
end
def has_hobby(hobby)
@hobbies << hobby
end
def has_friend(friend)
@friends << friend
end
每次实例化一个新人都会把它放到 PEOPLE 这个数组里。还有几个读属性,name,hobbies,friends。
initialize 方法里有个 name 变量,把它放到了 @name 属性里,同时也会初始化 hobbies 和 friends ,这两个属性在 has_hobby 与 has_friend 方法里用到了。
再完成 Person.method_missing :
def self.method_missing(m, *args)
method = m.to_s
if method.start_with?('all_with_')
attr = method[9..-1]
if self.public_method_defined?(attr)
PEOPLE.find_all do |person|
person.send(attr).include?(args[0])
end
else
raise ArgumentError, "Can't find #{attr}"
end
else
super
end
end
全部代码如下:
class Person
PEOPLE = []
attr_reader :name, :hobbies, :friends
def initialize(name)
@name = name
@hobbies = []
@friends = []
PEOPLE << self
end
def has_hobby(hobby)
@hobbies << hobby
end
def has_friend(friend)
@friends << friend
end
def self.method_missing(m, *args)
method = m.to_s
if method.start_with?('all_with_')
attr = method[9..-1]
if self.public_method_defined?(attr)
PEOPLE.find_all do |person|
person.send(attr).include?(args[0])
end
else
raise ArgumentError, "Can't find #{attr}"
end
else
super
end
end
end
j = Person.new("John")
p = Person.new("Paul")
g = Person.new("George")
r = Person.new("Ringo")
j.has_friend(p)
j.has_friend(g)
g.has_friend(p)
r.has_hobby("rings")
Person.all_with_friends(p).each do |person|
puts "#{person.name} is friends with #{p.name}"
end
Person.all_with_hobbies("rings").each do |person|
puts "#{person.name} is into rings"
end
执行的结果会是:
John is friends with Paul
George is friends with Paul
Ringo is into rings