2016年9月7日 上午10:18 ***
介绍一种跟类相似的构造:模块(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
下午3:53 ***
Ruby
评论
用self.method_missing()在class里面,这就定义成类方法了,对吧? 那Person类的实例应该就调用不到这个重写之后的method_missing,是这样吧?我没太理解
8 年 2 个月 以前
嘿嘿,等我更好的理解以后,在视频里做解释 :)
8 年 2 个月 以前
还有类似于这种Person.all_with_friends(p),我主要是没理解定义成类方法的好处?(虽然class也是对象)
8 年 2 个月 以前