类(Class)与模块(Module)

我们在对象和方法那一节中,提过这个概念。 比如 「人类」。

在面向对象概念中,一个类代表一组对象共同特征的抽象集合。比如「人类」,代表了人的共同特征,可以直立行走、会说话、会思考等人类特征。

*注:最好你可以打开irb或者是pry跟着练习*
在命令行输入 irb 或 pry,回车,你就进入到了一个互动的Ruby Shell界面里,你可以在里面输入代码,并且会马上得到运行结果。

chef-shell命令,就是基于irb来做的。

用Ruby代码表示就是:

class People
  def walk
    puts 'can'
  end

  def say
    puts 'hello word'
  end

  def think
    puts 'I got!'
  end
end

Ruby中用class关键字来声明一个类,注意类名People,是大写字母开头。没错,这个People,就是一个常量。

person = People.new

我们可以使用new方法来创建一个对象, 这里的person,就是我们创造的一个人。

person.walk
person.say
person.think

人类的行为, 这个person都可以做。

当你运行以上代码之后,会发现,这些方法都返回nil,这是因为Ruby的方法,如果你没有明确使用return,默认只返回方法内部最后一行的运行结果,上面的方法中,最后一行都是puts语句,puts语句会返回nil。

当然你可以使用return语句来给方法指定返回值。比如:

def walk
  return 'can'
end

但是他的名字呢,性别呢,种族或国籍等其他属性呢?这些个体的属性,是不可能每个人都一样的,那么我们该怎么设定呢?

class People
  def initialize(name="", gender="")
    @name = name
    @gender = gender
  end
end

我们打开类Peopole,使用initialize 方法,来给一个类添加属性。就是当你使用new方法创建一个对象的时候,这个对象可以被赋予属性。

上面的代码里, @name和@gender,都是实例变量, 而参数name,gender都是本地变量,也就是局部变量,它们可以被赋予默认值,但是,他们只能在initialize这个方法的作用域范围内有效,所以,叫本地变量。

这样,我们就可以重新创建一个person,指定name和gender了。


person = People.new('alex', 'man')
#=> #<People:0x007fb961313ce8 @gender="man", @name="alex">

当然,你仅仅创建了@name和@gender这俩实例变量,这还不够,你还不能给这俩实例变量赋值以及获取它们的值。如下:


person.name
#=> NoMethodError: undefined method `name' for #<People:0x007fb961313ce8 @name="alex", @gender="man">

所以,你必须要实现一对set/get方法。

我们再一次打开类,添加下面代码:

class People
  def name
    @name
  end

  def name=(name)
    @name = name
  end

  def gender
    @gender
  end

  def gender=(gender)
    @gender = gender
  end
end

然后我们马上再次执行下面代码:

person.name
#=> "alex"

返回了我们期望的结果。我们也可以给person改名:

 person.name = 'lee'
 person.walk
 person.say
 person.think

开放类

如果你跟着上面的代码走下来,会发现, 我们两次都是直接打开People这个类来修改,每次修改都没有重新添加之前的代码,而People这个类生成的对象的行为,却是只增不减。 尤其是你从其他语言转过来的话,比如Java,会感到奇怪。

没错,这正是Ruby的特性之一: Open Class,开放类。

Ruby的类是可以随便打开的,非常自由。

自由所带来的结果是,有可能会被滥用,比如,你可以打开Ruby内置的类来添加方法:


class String
  def to_iii
    self.to_i
  end
end

"111".to_iii
#=> 111

这种方式,有可能会影响到String类内置的方法,因为你无法记住每一个内置的方法,假如你添加的方法和内置的方法重名的时候,就完了,Ruby是不会警告你的,这样可能会引起非常严重的bug。

我们把这种方式叫做monkey patch。 意思就是这种方式,比较原始,就像猴子没进化到人这么高级一样。 不过Ruby2.1给出了一个方案,具体可以参考我的blog的相关文章:「Ruby2.1 Refinements」告别Monkey Patches,这里不再累述。

Chef的monkey patchs

在Chef这个工具里,也使用了这种方式:

链接:https://github.com/opscode/chef/blob/master/lib/chef.rb

可以看到:

require 'chef/monkey_patches/tempfile'
require 'chef/monkey_patches/string'
require 'chef/monkey_patches/numeric'
require 'chef/monkey_patches/object'
require 'chef/monkey_patches/file'
require 'chef/monkey_patches/uri'

先不用管require这个方法,这是Chef对于Ruby原生的类,添加了自己的方法。被添加的这个方法,应该是想要被所有的相关对象都可以响应。比如,我们打开「chef/monkey_patches/object」


class Object
  unless new.respond_to?(:tap)
    def tap
      yield self
      return self
    end
  end
end

可以看到, Chef这种添加方式还是比较安全的。它加了一层保护,「unless new.respond_to?(:tap)」, 只有Object的对象不能响应这个tap方法,它定义的这个tap方法才可被响应。

这样起到一个很好的保护作用。

模块

模块,使用module关键字来定义的,你可以把它当做一组方法集合。比如有一群人,他们有共同的兴趣,比如踢球、看球、打dota。 但是这些兴趣并不是所有人类都有的,这个时候就需要模块了。

module Interest

  def kickball
    puts  "i like kicking ball"
  end

  def read
    puts  "i like reading books"
  end

  def dota
    puts  "i like playing dota"
  end

end

然后我们就可以为某个人赋予这些兴趣:

person = People.new('张三', 'man')
person.extend Interest

person.kickball #=> "i like kicking ball"
person.read     #=> "i like reading books"
person.dota     #=> "i like playing dota"

注意,这里person是一个对象,我们可以使用extend来把Interest模块的方法来拓展给person,让他也拥有这些兴趣。这种方式,叫做Mixin。

模块,也可以作为命名空间来使用:

class Chef
  module Mixin

     module Command

       # ...

     end

     module Template

       # ...

     end

  end

end

这样,我们可以使用Chef::Mixin::Command和Chef::Mixin::Template来使用这两模块,这就起到一个命名空间的作用。

继承

我们可以使用模块来为类添加一组方法, 当然也可以使用继承来添加一个子类。 那我们刚才的例子来说,有一组相同兴趣的人群, 他们首先是人类,然后才是有这种兴趣的人。


class Fan < People
  def kickball
    puts  "i like kicking ball"
  end

  def read
    puts  "i like reading books"
  end

  def dota
    puts  "i like playing dota"
  end
end

我们定义一个类Fan, 使用 < 关键字来让这个类继承自People,那么Fan就拥有了People的所有属性和行为。

person = Fan.new('李四', 'man')
person.kickball #=> "i like kicking ball"

同样,李四和前面的张三,拥有了相同的对象。

我们可以把继承和模块结合起来使用, 因为兴趣分很多类型,有运动、阅读、打游戏,而运动种类,阅读的种类和游戏的种类也很多,结合起来使用,会让我们的代码更加灵活:

module SportInterest
  def walk; 'walk' end
  def run; 'run' end
  def swimming; 'swimming' end
  # ...
end

module ReadInterest
  def read_book; 'read book' end
  def watching_tv; 'watching tv' end
  # ...
end

module GameInterest
  def cs; 'play cs' end
  def dota; 'play dota' end
  def wow; 'play wow' end
  # ...
end

class SportFan < People
  include SportInterest
end

class ReadFan < People
  include ReadInterest
end

class GameFan < People
  include GameInterest
end

这样,我们可以随便为各个兴趣模块里面添加各种项目,也不会影响到相关的Fan类。

再加上命名空间的概念,重构下上面代码,就是下面这样:

module Interest
  module Sport
    def walk; 'walk' end
    def run; 'run' end
    def swimming; 'swimming' end
    # ...
  end

  module Read
    def read_book; 'read book' end
    def watching_tv; 'watching tv' end
    # ...
  end

  module Game
    def cs; 'play cs' end
    def dota; 'play dota' end
    def wow; 'play wow' end
    # ...
  end
end


class SportFan < People
  include Interest::Sport
end

class ReadFan < People
  include Interest::Read
end

class GameFan < People
  include Interest::Game
end

这样,代码是不是看着更清晰了。