Ruby学习笔记之Ruby 模块

作者:袖梨 2022-06-25

介绍一种跟类相似的构造:模块(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
=> #
>> obj.blah
NoMethodError: undefined method `blah' for #
 from (irb):2
 from /usr/local/bin/irb:11:in `

'
我们可以覆盖 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

相关文章

精彩推荐