天天看點

ruby way之進階OOP特性之一

1 發送一條消息給一個對象

當你調用一個方法時,你也就是發送了一條消息給一個對象,在ruby中我們能夠在運作時決定那個方法被調用。send 方法就是做這個的,他接受一個symbol為參數.

舉個簡單的例子,假設我們要寫一個排序,我們想要使用不同的域作為比較的key。雖然我們這時可以用block,可是如果使用send的話,我們能有一個更優美的寫法:

class Person
  attr_reader :name, :age, :height

  def initialize(name, age, height)
    @name, @age, @height = name, age, height
  end

  def inspect
    "#@name #@age #@height"
  end
end


class Array
  def sort_by(sym)   # Our own version of sort_by
    self.sort {|x,y| x.send(sym) <=> y.send(sym) }
  end
end


people = []
people << Person.new("Hansel", 35, 69)
people << Person.new("Gretel", 32, 64)
people << Person.new("Ted", 36, 68)
people << Person.new("Alice", 33, 63)

p1 = people.sort_by(:name)
p2 = people.sort_by(:age)
p3 = people.sort_by(:height)

p p1   # [Alice 33 63, Gretel 32 64, Hansel 35 69, Ted 36 68]
p p2   # [Gretel 32 64, Alice 33 63, Hansel 35 69, Ted 36 68]
p p3   # [Alice 33 63, Gretel 32 64, Ted 36 68, Hansel 35 69]
           

__send__ 其實也就是send方法的别名了。不過這邊建議用__send__,這是因為send有可能作為一個使用者自己定義的方法。

在1.9中,send方法不能調用private方法了,不過我們能夠使用__send!來調用:

class Foo   
  private   
  def foo   
    "aa"   
  end   
end   
p Foo.new.__send!(:foo)     # => nil   
p Foo.new.send(:foo)      #private method `foo' called for #<Foo:0xa89530> (NoMethodError)  
           

2 特殊化一個單獨的對象

在很多oo語言裡,一個類的所有的對象共享相同的行為,類作為一個模闆,當構造器調用時,制造一個擁有相同接口的對象。

ruby中我們可以改變一個對象運作時的狀态。你可以給對象一個私有的,匿名的子類:所有的原始的方法都是可用的。由于聯系到這個對象上的行為是私有的,是以它隻發生一次。一件隻發生一次的事叫做“singleton”,比如singleton 方法,和singleton類.

看下面的代碼:

a = "hello"
b = "goodbye"

def b.upcase      # create single method
  gsub(/(.)(.)/) { $1.upcase + $2 }
end

puts a.upcase   # HELLO
puts b.upcase   # GoOdBye
           

加一個singleton 方法到一個對象,也就是建立了一個singleton 的類,然後這個類的父類是這個對象的類,如果你加多個方法到一個對象,這是你可以直接實作一個singleton 類:

b = "goodbye"

class << b

  def upcase      # create single method
    gsub(/(.)(.)/) { $1.upcase + $2 }
  end

  def upcase!
    gsub!(/(.)(.)/) { $1.upcase + $2 }
  end

end

puts b.upcase  # GoOdBye
puts b         # goodbye
b.upcase!
puts b         # GoOdBye
           

這裡要注意的是,一些更“primitive”的對象(比如Fixnum),不能加singleton 方法,這是因為這些對象,不是存引用在記憶體中的。但是在ruby将來的版本,可能會實作這個。

在一些庫的源碼中,我們能看到這種代碼:

在一個類的體内,self 就指這個類自己,在這個類中的執行個體方法,其實也就是外面類的類方法:

class TheClass
  class << self
    def hello
      puts "hi"
    end
  end
end

# invoke a class method
TheClass.hello            # hi
           

使用這個技術的另一個原因是,可以建立一個類級别的幫助方法,然後我們就能在這個類的其他地方使用它了.

class MyClass

  class << self

    def accessor_string(*names)
      names.each do |name|
        class_eval <<-EOF
          def #{name}
            @#{name}.to_s
          end
        EOF
      end
    end

  end

  def initialize
    @a = [ 1, 2, 3 ]
    @b = Time.now
  end

  accessor_string :a, :b

end


o = MyClass.new
puts o.a           # 123
puts o.b           # Mon Apr 30 23:12:15 CDT 2001
           

extend 方法能夠mix一個子產品到一個對象:

module Quantifier

  def any?
    self.each { |x| return true if yield x }
    false
  end

  def all?
    self.each { |x| return false if not yield x }
    true
  end

end

list = [1, 2, 3, 4, 5]

list.extend(Quantifier)

flag1 = list.any? {|x| x > 5 }        # false
flag2 = list.any? {|x| x >= 5 }       # true
flag3 = list.all? {|x| x <= 10 }      # true
flag4 = list.all? {|x| x % 2 == 0 }   # false
           

3 建立一個帶參數的類

假設我們想要建立一個多樣的類,也就是說,可以通過控制類變量來控制它的多種狀态:

class Terran

  @@home_planet = "Earth"

  def Terran.home_planet
    @@home_planet
  end

  def Terran.home_planet=(x)
    @@home_planet = x
  end

  #...

end
           

這樣是可以的,這時如果我想要定義一些與Terran類似的類,你可能會馬上想到是可以給這些類抽象出來一個超類就行了:

(注意,這裡是錯誤的方法)

class IntelligentLife   # Wrong way to do this!

  @@home_planet = nil

  def IntelligentLife.home_planet
    @@home_planet
  end

  def IntelligentLife.home_planet=(x)
    @@home_planet = x
  end

  #...
end

class Terran < IntelligentLife
  @@home_planet = "Earth"
  #...
end

class Martian < IntelligentLife
  @@home_planet = "Mars"
  #...
end
           

當你調用Terran.home_planet時,在1.9中會列印出nil,在1.8中會列印出Mars.

為什麼會這樣?答案是class variables 不是真正的class variables 。他們不屬于類,而是屬于整個繼承體系。class variables不能從父類所被複制,但是能夠從父類所被共享。

我們可以剔除掉類變量在基類中的定義,可是這時我們定義的類方法就不能工作了。

這裡有一個稍好一些的方法,使用了class_eval 方法:

class IntelligentLife

  def IntelligentLife.home_planet
    class_eval("@@home_planet")
  end

  def IntelligentLife.home_planet=(x)
    class_eval("@@home_planet = #{x}")
  end

  #...
end

class Terran < IntelligentLife
  @@home_planet = "Earth"
  #...
end

class Martian < IntelligentLife
  @@home_planet = "Mars"
  #...
end


puts Terran.home_planet            # Earth
puts Martian.home_planet           # Mars
           

這個可以列印出我們想要的結果任何IntelligentLife裡定義的執行個體變量,或者執行個體方法,都會被Terran 和 Martian所繼承。

下面的方法可能是最好的方法,我們沒有使用類變量,而是使用類執行個體變量:

這裡我們打開了一個singleton class,定義了一個存取方法home_planet,兩個子類調用他們自己的accessors 來設定變量.我們其實還可以給home_planet=方法設定為 private的。

這裡其實還可以這樣做:

module IntelligentLife
    attr_accessor :home_planet
end

class Terran
  class << self
    include IntelligentLife
  end
 self.home_planet = "Earth"
  #...
end

class Martian
  class << self
    include IntelligentLife
  end
  self.home_planet = "Mars"
  #...
end


puts Terran.home_planet            # Earth
puts Martian.home_planet           # Mars
           

4 使用Continuations 來實作一個生成器

ruby的一個更抽象的特性就是continuation。這是一種控制非局部的跳轉和傳回的方法。一個continuation 對象存儲着一個傳回的位址,和一個上下文.他看起來很像c中的setjmp/longjmp ,可是他存儲着更多的上下文.

Kernel 的方法callcc接受一個block,傳回一個Continuation 對象。這個被傳回的對象作為一個參數被傳遞進這個block.Continuation唯一的方法就是call,調用它将會引起一個非局部的傳回,執行callc的block直到它的尾部。

其實Continuation很像遊戲中的儲存遊戲的特性。最好的了解Continuation的方法,就是去看電影Run, Lola, Run (哈哈,就是羅拉快跑).

Continuation的最好的例子就是生成器,現在生成器已經是ruby的一部分了。

請看下面的使用生成器的Fibonacci numbers 的例子:

class Generator

  def initialize
    do_generation
  end

  def next
    callcc do |here|
      @main_context = here;
      @generator_context.call 
    end
  end

  private

  def do_generation
    callcc do |context|
      @generator_context = context;
      return
    end
    generating_loop
  end
  def generate(value)
    callcc do |context|
      @generator_context = context;
      @main_context.call(value)
    end
  end
end

# Subclass this and define a generating_loop

class FibGenerator < Generator
  def generating_loop
    generate(1)
    a, b = 1, 1
    loop do
      generate(b)
      a, b = b, a+b
    end
  end
end


# Now instantiate the class...

fib = FibGenerator.new

puts fib.next            # 1
puts fib.next            # 1
puts fib.next            # 2
puts fib.next            # 3
puts fib.next            # 5
puts fib.next            # 8
puts fib.next            # 13
           

這裡要注意的是,continuations的性能不好,因為它儲存了太多的狀态和上下文...

繼續閱讀