从前面我们演示的例子,你可能会对我们早先所宣称的Ruby是一种面向对象的语言而感到奇怪。
那么,我们通过这章内容来证明它。我们将要介绍怎样使用Ruby新建类和对象,并介绍Ruby在哪些方面比大部分的面向对象语言要更强大。
让我们一步步地实现一个百万美元的产品,Internet Enabled Jazz and Bluegrass自动唱机的一部分。
在数月的工作后,我们那些高收入的研究和开发人员确定,我们的自动唱机需要歌。因此新建一个Ruby类来描述歌曲是个不错的主意。
我们知道,一首真正的歌有名字,演唱者和时间,因些我们要确保在我们的程序中歌的对象也是这样子的。
让我们开始新建一个基类Song,这只包含一个方法initialize。
class Song
def initialize(name, artist, duration)
@name = name
@artist = artist
@duration = duration
end
end
initialize是Ruby程序中一个特殊方法。当你调用Song.new来创建一个新的Song对象时,Ruby分配一些内存来存放一个还没初始化的对象,
然后调用该对象的initialize方法,传入传递给new的所有参数。这个方法用于建立对象的状态。
在Song类中,initialize方法有三个参数。这些参数就像是方法内的局部变量一样,因此它们遵循局部变量的命名约定,使用小写字母开头。
每个对象都对应着自己的歌曲,因此我们需要这些Song对象保存自己的歌曲名,演唱者和时间。也就是说,这些值在对象中要保存为实例变量。
实例变量能被对象中所有的方法访问,每个对象都有自己的实例变量的副本。
在Ruby中,实例变量是名字前面简单地加上一个"at"符号(@)。在我们的例子中,参数name赋给实例变量@name,artist赋给@artist,
duration(歌曲的长度,以秒为单位)赋给@duration。
让我们测试一下我们漂亮的新类。
song = Song.new("Bicylops", "Fleck", 260)
song.inspect -> #<Song:0x1c7ca8 @name="Bicylops", @duration=260, @artist="Fleck">
不错,它看上去能用了。inspect信息默认能被传入到任何的对象中,格式化对象的ID和实例变量。从中可以看到,我们已经正确的建立它们了。
经验告诉我们,在开发中,我们会多次打印歌曲的内容,而inspect的默认格式有一些我们想要的信息。幸运地,Ruby有一个标准的信息,to_s,
它传给任何想以字符串显示的对象。让我们在我们的歌曲上试用下它。
song = Song.new("Bicylops", "Fleck", 260)
song.to_s -> "#<Song:0x1c7ec4>"
这不是很实用——它只是返回对象的ID。因此, 让我们在类中重写它。在这之前,让我们讨论一下在本书中我们是怎样定义一个类的。
在Ruby中,类是永远不闭合的:你可以随时在一个现有类中添加方法。它适用于你写的类和标准的内置类。
对一个现有的类开放类的定义,你指定的新内容将会添加到任何的类中。
这是我们的重要目的。在我们深入本章之前,给我们的类添加个些特性,我们将只演示这个类定义的新方法;原来的那个类仍然存在。
这省去了在每个例子中重复多余的内容。显然,如果这些新建的代码你是乱写的,你可能会把所有的方法写在一个单独的类定义中。
已经有足够的了解了。让我们给我们的Song类添加一个to_s方法吧。我们将在字符串中使用#字符过来插入这三个实例变量的值。
class Song
def to_s
"Song: #@name--#@artist (#@duration)"
end
end
song = Song.new("Bicylops", "Fleck", 260)
song.to_s -> "Song: Bicylops--Fleck (260)"
很好,已经有了改进了。然而,我们把一些比细微的东西混合在了一起。我们说过,Ruby所有的对象都支持to_s,但我们没说怎样支持。
这个答案要涉及到继承,子类和当一个对象接收到消息时Ruby是怎么确定要运行哪个方法。这个话题是将留给新的章节,因此...
继承和信息
继承允许你创建这样一个类,它是另一个类的具体实现或特殊实现。
例如,我们的自动唱机有歌曲这个概念,我们把它封装在Song类中。市场需求告诉我们,我们需要提供卡拉OK的支持。
卡拉OK歌曲和其它歌曲(没有音轨,我见过这与我们无关)很相似。然后它还带有歌词和时间信息。当我们的自动唱机播放卡拉OK时,
歌词要和音乐要同步地显示在自动唱机的屏幕上。
其中一个解决问题的方法是定义一个新类KaraokeSong,它和Song一样,只是多了个歌词的轨道。
class KaraokeSong < Song
def initialize(name, artist, duration, lyrics)
super(name, artist, duration)
@lyrics = lyrics
end
end
在类定义行上的"< Song"告诉Ruby,KaraokeSong是Song的一个子类。(没什么惊讶的,也就是说Song是KaraokeSong是超类。
人们也全用父子关系来描述,因此KaraokeSong的父亲是Song。)现在,不用担心它的继承方法;我们呆会再解释super的调用。
让我们创建一个KaraokeSong看看它是否能运行。(在最终的系统中,歌词保存在一个包含有文字和时间信息的对象里)我们只使用
一个字符串来测试我们的类。这是动态语言的另一个好处——我们不需要在代码运行之前定义任何东西。
song = KaraokeSong.new("My Way", "Sinatra", 225, "And now, the...")
song.to_s ! "Song: My WaySinatra (225)"
不错,它已经运行了。但为什么在to_s方法中没有显示歌词呢?
这个问题需要回答当你给对象传递信息时,Ruby决定哪个方法应该被调用的方式。
在最初传递的程序代码中,当Ruby发现song.to_s的方法调用时,它实际上并不知道在哪里找方法to_s。
取代的,它延迟这个决定直到程序的运行。在这个时候,它查找song类。如果这个类实现了和作为信息传递给它的相同名字的方法,
那么就运行这个方法。否则Ruby就在它的父类中查找这个方法,然后是父父类,一直往上。如果还是找不到适当的方法,
它就执行一个特殊的动作,抛出错误。
回到我们的例子。我们传递信息to_s给song,它是KaraokeSong类的一个对象。Ruby在KaraokeSong中查找叫to_s的方法但没有找到。
这个编译器然后查找KaraokeSong的父类,Song类,在那里,它找到了我们定义的to_s方法。这就是为什么它打印出了song的详细信息但
没有歌词类的原因,因为Song并不知道lyrics。
让我们实现KaraokeSong的to_s方法来修正它。你可以有多种实现方式。让我们从不好的方式开始。我们从Song中复制to_s过来,然后添加lyric。
class KaraokeSong
# ...
def to_s
"KS: #@name#@
artist (#@duration) [#@lyrics]"
end
end
song = KaraokeSong.new("My Way", "Sinatra", 225, "And now, the...")
song.to_s -> "KS: My WaySinatra (225) [And now, the...]"
我们正确地显示了实例变量@lyrics的值。在这个方法中,子类直接访问父类的实例变量。
那么为什么它是一个不好的实现to_s的方式呢。
这个答案和良好的编程方式有关(解耦)。通过查看父类内部的结构,明确地使用它的实例变量,代码之间产生了耦合。
如果我们决定修改Song保存歌曲时间的单位是毫秒。突然地,KaraokeSong将会显示一个荒谬的值。
对于在各自的类中都有各自的实现细节这个问题,当KaraokeSong#to_s调用时,我们将先调用它父类的to_s方法获得歌曲的详细信息。
然后再把歌词信息追加到它后面再返回结果。
这里用到了Ruby的关键字super。当你不传递参数调用super时,Ruby传送一个信息给当前对象的父类,让它调用父类的同名方法。
它传入的方法参数是原先调用的方法所传入的参数。现在,我们能实现新的改进了的to_s。
class KaraokeSong < Song
# Format ourselves as a string by appending
# our lyrics to our parent's #to_s value.
def to_s
super + " [#@lyrics]"
end
end
song = KaraokeSong.new("My Way", "Sinatra", 225, "And now, the...")
song.to_s -> "Song: My WaySinatra (225) [And now, the...]"
我们明确地告诉Ruby KaraokeSong的父类是Song,但我们没有明确地指出Song的父类。
当你定义一个类时如果没有明确指明父类,Ruby默认提供的是Object类。也就是说,在Ruby中所有对象都有一个祖先Object,Object的所有实例方法对每个对象都可用。