Ruby學習筆記之Ruby 模組

來源:互聯網
上載者:User

介紹一種跟類相似的構造:模組(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

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.