從前面我們示範的例子,你可能會對我們早先所宣稱的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的所有執行個體方法對每個對象都可用。