Ruby对象模型

在了解了对象和消息之后,让我们一起来看看Ruby中的对象模型实现。

Class

class Baby
  def say
    puts "hello world!"
  end
end

class 关键字来定义一个类,我们命名为Baby,同时我们定义个方法say,以Ruby的惯例,我们用Baby#say来表示这个方法,意指这是Baby类的实例方法。 所谓实例方法,就是一个类生成的实例对象才能响应的方法。

baby = Baby.new
baby.say #=> "hello world!"
baby.class #=> Baby

我们创建一个baby对象,baby响应了一个class消息,返回了baby的类。class消息,说的更通俗一些,就是class方法,是Ruby内建的方法,帮助你了解一个对象是来自于哪个类。

「Ruby中一切皆对象」,念念不忘,必有回想。

既然这样,Baby类难道也是一个对象?那么让我试试看Baby类能否响应class方法:

Baby.class #=> Class

Baby类响应了class方法,返回了一个类:Class

让我们继续思考:

Class.class #=> Class
Class.class #=> Class
Class.class #=> Class”

Class响应了class方法,返回了Class类,不管你执行多少遍代码,返回的依旧是Class。

看到这里,你是不是明白了什么呢?

我们用一张图来表达:

mvc

这说明:Ruby中任意一个类,实际是Class的一个实例对象。Class本身也是自己的一个实例对象。

klass = Class.new  #=> <Class:0x007ff03ad71220>
klass.class        #=> Class

看到这里,不知道有没有人和我有一样的疑问,那个class方法,是在哪里定义的呢?好神奇。

class Baby
end

但是Baby类,我们并没有继承任何类。我们回头看上文中的代码:

Baby.class #=> Class

Baby的类是Class,那么这个实例方法,应该是被定义在Class中的了。只有这样,Ruby中的任意类,才可以响应class方法。

SuperClass

让我们继续了解关于Class秘密。

Ruby是正宗的OOP语言,OOP的一个典型的特征是继承。

class Father; end
class Baby < Father; end

上例中,Baby类继承于Father类,Baby类叫子类,Father类叫父类(也叫超类)。Ruby中用小于号(<)来实现继承。

Ruby中既然有方法class可以让我们了解一个对象的类,应该也有方法让我们了解一个类的父类(超类),那就是superclass方法。

Baby.superclass        #=> Father
Father.superclass      #=> Object
Object.superclass      #=> BasicObject
BasicObject.superclass #=> nil

我们可以明显的看出来这样一个继承链:

Baby < Father < Object < BasicObject

nil是Ruby中表示空对象的一个值,nil.class是NilClass。BasicObject响应superclass方法返回nil,说明BasicObject已经没有父类(超类)类,BasicObject已经是顶层类了。

当然,这些类也必然都是Class的对象了。

Object.class      #=> Class
BasicObject.class #=> Class

那么Class有没有父类呢? 好奇的我,决定试试:

Class.superclass #=> Module

Module? 让我们继续:

Module.superclass #=> Object
Object.superclass #=> BasicObject

又回到原点了。

用一张图来表示:

mvc

其实Ruby中还提供一个方法,可以让我们清晰的看到一个类的继承链。

Baby.ancestors
#=> [Baby, Father, Object, Kernel, BasicObject]

ancestors,英文单词有「祖先」的意思,所以这个方法顾名思义,会返回Baby类的继承体系祖先链。基本和我们上面列的继承链是一样的,但是我们看到多出一个Kernel来。

Module

让我们看看Kernel是个是什么?用class方法试试,看它返回什么信息。

Kernel.class #=> Module

原来它是Module。Module我们之前见过,它本身也是一个类,同时是Class类的超类。那么Module到底是什么呢?有什么用?

其实,Ruby中除了正常的类之外,还有一种特殊的类叫做模块。

module Behavior; end

用module关键字定义的特殊类,就叫做模块。

Behavior.class #=> Module

可以看得出来,模块是Module的实例对象。

综合前文所说,可以做一个总结:

类,是Class的实例对象。 模块,是Module的实例对象。 注意我们的描述: 英文「Module」,是指Module类。 中文「模块」,是指Module类的实例对象。

「模块」是一种特殊的类,那么它特殊在哪里呢?让我用类的概念来试探一下:

behavior = Behavior.new
#=> NoMethodError: undefined method `new' for Behavior:Module

当我们给Behavior模块发送new消息的时候,报错。错误信息很明显:「模块Behavior没有定义new方法」, 意思就是模块无法响应new方法,而我们知道,new方法是类用来创建实例对象的,所以,模块的一个特性就被我们了解了,那就是:模块无法创建实例对象。

有的人可能就纳闷了,一个无法创建实例对象的类有什么用?

Mixin

再回到OOP的概念里,面向对象除了继承的特性之外,还有多态。

可能学过Java的朋友会想,Ruby是弱类型动态语言,没有接口,没有抽象类,怎么实现多态呢?

请忘记你学的Java吧。多态是面向对象语言的特性,Ruby中当然有,只是实现方式不同而已。 Java中用接口实现多态的方式,只是Java自己的实现,不要用Java的思维来思考Ruby。这也是很多从Java转向Ruby程序员的通病。

Ruby中实现多态是利用一种叫Duck Typing的特性,中文翻译叫「鸭子类型」, 意指:在Ruby这种弱类型语言中,我们应该关注一个对象的行为,而不是它的类型。就好像哪怕是一只「鸡」,叫声如果像鸭子,那么就可以把它当鸭子对待。

Duck Typing是Ruby的精髓。

class Duck
  def cry
    "ga...ga...ga"
  end
end

class Chicken
  def cry
    "gua ... gua ... gua"
  end
end

class Person
  def kill(fowl)
    fowl.cry
  end
end

person = Person.new
duck = Duck.new
chicken = Chicken.new

person.kill duck       #=> "ga...ga...ga"
person.kill chicken    #=> "gua ... gua ... gua"

上面代码中,Duck和Chicken都属于家禽的一种,某个人person,宰杀鸭子和宰杀鸡的时候,它们都会cry,叫声还差不多。

这就叫多态。

我们可以看到上面的代码,Duck和Chicken两个类都实现了cry方法,如果再有更多的类,我们需要实现更多的cry方法,而面向对象编程的初衷就是,为了方便代码复用,这样做显然不科学。

这时候模块就该出场了。

模块有一个特性,叫做Mixin,中文翻译为「混入」。Mixin本质上可以理解为Ruby实现多继承的一种方式,不管你有多少个模块,mixin到一个类里, 这些模块就会「挂」到这个类的祖先链(单继承树)上面。

module Behavior
  def cry
    "ga ga ga"
  end
end

class Duck
  include Behavior
end

class Chicken
  include Behavior
end

我们使用include方法,就可以把模块Behavior的代码混入到Duck类和Chicken类中,让Duck和Chicken都可以响应cry方法。这样代码就达到了复用的效果。

但是,这样做很明显的,Duck和Chicken的cry完全一样了,这样还不是很科学。

让我们继续改造。

封装

OOP还有一个重要的特性:封装。

所谓封装,就是把对象的属性和行为封装为一个独立的单位,尽可能隐藏细节。

回到前文中的代码示例,对于Duck和Chicken而言,cry是一种行为,cry的声音是一种属性。

class Duck
  attr_accessor :sound

  def initialize
    @sound = "ga...ga...ga"
  end
end

class Chicken
  attr_accessor :sound

  def initialize
    @sound = "gua...gua...gua"
  end

end

duck = Duck.new
duck.sound #=> "ga...ga...ga"
chicken = Chicken.new
chicken.sound #=> "gua...gua...gua"

这样,我们就实现了属性的封装。然后修改我们的模块Behavior, 完整代码如下:

module Behavior
  def cry
    sound
  end
end

class Duck
  include Behavior

  attr_accessor :sound

  def initialize
    @sound = "ga...ga...ga"
  end
end

class Chicken
  include Behavior

  attr_accessor :sound

  def initialize
    @sound = "gua...gua...gua"
  end
end

duck = Duck.new
duck.cry #=> "ga...ga...ga"
chicken = Chicken.new
chicken.cry #=> "gua...gua...gua"

这样,就优雅的实现了我们的需求。

类方法

以上,我们一直都在说的是,对象间的通信。 那么类之间该如何通信呢?或者说,怎么样给类发送一个消息?

可能有人会想,类也是个对象嘛。

恭喜你,猜对了。

给类发送消息,和给对象发送消息是一样的。

class Person
  def self.eat(food)
    puts food
  end
end

Person.eat "meat" #=> "meat"

用我们常用的「.」给它发消息就可以了,只要它能响应,它就会返回结果。如果不能响应,它则会报错。

但是细心的人注意到了,这个地方定义的方法和上面的不一样:

def self.eat

这里多了一个self。 self,顾名思义,是自己的意思。 你在一个类里,定义个方法,指明了这个消息接收者是self, 那就是说,这个消息是自己接收的, 这里的「自己」就是指Person Class。所以,这样定义的方法叫类方法。

可能有的人要问了, 你上面定义的实例方法,也没指定接收者啊。

是的,我的确没有指定一个「显式」的接收者。但是还有一个「隐式」的接收者。

class Person
  def eat(food)
    puts food
  end
end

scope

Ruby中的class、def、module关键字,实际上定义了各自的作用域,也就是说,它们有各自的地盘。

class Person
  puts self

  def eat
    puts self
  end

  def self.sing
    puts self
  end
end

#=> Person

Person.sing  #=> Person
Person.eat   #=> NoMethodError: undefined method `eat' for Person:Class
Person.new.eat #=> #<Person:0x007febd4a38cd0>

打印不同的scope的self,我们可以了解,class、def都定义了自己的域,并相互隔离。

class定义的是只有类才能访问的地盘。 def 一般定义的scope是只有实例对象才能访问的地盘,也就是实例方法。 而def如果在类地盘上指定了显式的接收者self,这时候self是指类本身,所以定义的是一个类方法。

加显式self的时候,一定要注意self是在哪一个scope里。

而隐式self,实际就是那个默认可以响应这个scope里消息的接收者。

比如上例中的eat方法,默认的scope是实例对象的,所以它是只能实例对象响应的方法。

SingletonClass

定义类方法,还有一种写法:

class Person
  class << self
    def eat(food)
      puts food
    end
  end
end

Person.eat('meat') #=> "meat"

注意,我们这里是在 class << self ... end 这样一个域里面定义的方法。

好吧,告诉你真相:

class Person
  class << self
    puts self
  end
end

#=> #<Class:Person>

这里打印了class << self , 这个self是指#对象。

我们再来看看这到底是个什么东东。

class Person
  @person = class << self
    self
  end

  def self.born
    puts @person.new
  end
end

Person.born
#=> TypeError: can't create instance of singleton class

当我给Person发送born消息,返回一个错误,翻译过来就是: 类型错误: 不能给一个singleton class创建实例。

噢,原来这个 class << self 是singleton class。

我们继续探索:

class Person
  @person = class << self
    self
  end

  def self.born
    puts @person.class
    puts @person.superclass
  end
end

Person.born
#=> Class
#=> #<Class:Object>

我们可以看到, class << self这个singleton class,它还是Class的对象(当然,每个类都是Class的对象),但是它的父类(超类)是一个奇怪的

#<Class:Object>

, 和刚才的

#<Class:Person>

格式一样,看来,这个

#<Class:Object>

也是一个Singleton Class。 而正常的类Person的superclass,是Object。

class Object
  class  << self
    puts self
  end
end

#=> #<Class:Object>

我们把Object类的Singleton Class 打印出来(注:因为Ruby的开放类特性,这里不是重新定义Object类),看到也是

#<Class:Object>

那么这俩是同一个吗? 我们继续代码验证:

class Object
  class  << self
    puts self.object_id
  end
end

#=> 70108374406880

class Person
  @person = class << self
    self
  end

  def self.born
    puts @person.class
    puts @person.superclass.object_id
  end
end

Person.born
#=>Class
#=> 70108374406880

可以看得出来,打印了两个singleton class的object_id ,都是70108374406880,足以证明,他们是同一个对象了。

那么说了这么多,singleton class和类方法有什么关系呢?

现在回想一下, 我们创建普通类的实例方法,是在普通类开辟了一个实例对象的地盘(scope),而定义类方法, 我们实际是开辟了一块类的私有区域(scope),也就是类的singleton class。

class Person
  def self.born
    puts "baby"
  end

  class << self
    def eat(food)
      puts food
    end
  end
end

Person.born #=> "baby"
Person.eat("meat") #=> "meat"
Person.singleton_methods #=> [:born, :eat]

可以知道,def self.xxx 这种形式,实际上也是开辟了一个singleton class域,方法定义于这个私有区域里面,供类来使用。

当然了,我们能指定显式接收者是self,也可以指定其他对象,比如:

class Person; end

person1 = Person.new
person2 = Person.new

def person1.eat(food)
  puts food
end

person1.eat("meat") #=> "meat"
person2.eat("meat") #=> NoMethodError: undefined method `eat' for #<Person:0x007f86c1a87e48>

上例中,如果把一个实例对象作为接收者定义一个方法,那么这个方法就只能响应这个对象person1, 而person2对象无法响应这个方法,所以报错。

其实每个实例对象,也有自己的singleton class:

class Person; end

person1 = Person.new #=> #<Person:0x007f86c21e3458>

class << person1
  puts self
end

#=> #<Class:#<Person:0x007f86c21e3458>>

可以看得出来,这个singleton class是那个person1实例对象的singleton class。

我们可以叫它「单类」。

它只是跟随每个正常的类,包括Class类自己,都有一个单类的存在:「#」,你可以自己写代码去验证一下。

所以,到现在为止,我们应该明白了,什么是singleton class。

singleton class就是存在于Ruby中每个对象中,和对象形影不离,而实例对象是自己本身的一个隐藏类。

Ruby在1.9版本之后,也提供给我们一个很方便的内建方法,可以方便的使用单类:

class Person
  puts singleton_class
end

Ruby中每个对象都有自己的Singleton Class这样一个单类。

Ruby的实例方法被定义在普通类中, 而Ruby的类方法,被定义在它的单类(Singleton Class)中, 所谓的类方法,实际上是一个类的专属方法。

Ruby中的class、def、module关键字,会定义一个scope。scope之间是相互隔离的。

示例,这段代码:

class Person
  def eat
    puts "eat food"
  end

  def self.sing
    puts  'song'
  end

  class << self
    def born
      puts "baby"
    end
  end

end

用linux文件系统类比就是下面这样:

person_class/
  |_ .singleton_class /
  |     |_ sing
  |     |_ born
  |_ eat

方法查找

到目前为止,我们大概了解了Ruby的对象模型,那么在这种对象模型之下,消息该怎么传递呢。

类之间又有继承,又有模块的Mixin,我们前面讲过,Mixin实为多继承。看示例:

class A
  def t
    puts 'in a'
  end
end

module B
  def t
    puts "in b"
  end
end

class C < A
  include B

  def t
    puts 'in c'
  end
end

c = C.new
c.t

面对这样的代码,你知道c.t 响应的是哪个方法吗?

你可能会说,C里面定义了t方法,当然会输出「in c」啊。

那么咱们把C里面的t方法去掉,你知道c.t会响应哪个方法吗?

如果想不通,就让我教你一个最简单的方法:

C.ancestors

#=> [C, B, A, Object, Kernel, BasicObject]

C的ancestors方法,可以返回C类的祖先链,也就是一个继承树。

Ruby是单继承,Ruby之父为了简化多继承,用Mixin的方式,把模块也挂载到这样一个单继承树上面,所以我们一目了然C的所有继承关系。

而Ruby中的消息传递,是沿着这个继承树往上查找的。

比如c.t这个消息,如果C类中没有定义这个方法,那么就会去B里面找,B里定义了t方法,就会响应,从而输出「in b」。如果B里没有定义t方法,就会找到A,直到继承树顶端。

上面只是没有考虑singleton class的情况,如果我们的代码中包含了singleton class,那么方法查找的顺序就会有所改变:

class Person
  def eat
    puts "food"
  end

  def say
    puts "hello"
  end
end

person = Person.new
class << person
  def eat
    puts "meat"
  end

end

上面这个例子,当你执行person.eat的时候,该是哪个方法会被响应?person.say呢?

事实证明,person.eat输出的是"meat",所以方法查找会先从singleton class开始,如果找不到,会“沿着这个singleton class的父类(超类)往上查找,当你执行person.say的时候,就会到Person这个类中去查找。 这是因为person.singleton_class.superclass 就是Person。

总结

以上,就是Ruby的对象模型。如果你理解了Ruby的对象模型,那么Ruby的代码,基本就可以在你脑中运行了。