类(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
这样,代码是不是看着更清晰了。