💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
# 类(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](http://tao.logdown.com/posts/171266-ruby21-refinements-farewell-to-monkey-patches),这里不再累述。 ### Chef的monkey patchs 在Chef这个工具里,也使用了这种方式: 链接:[https://github.com/opscode/chef/blob/master/lib/chef.rb](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 ~~~ 这样,代码是不是看着更清晰了。