第三章:方法與物件 ( Methods & Classes )
身為一個優秀的程式設計師總是會犯職業病,以山為壹,以水為零,記憶體裡的資料就是一堆二進位開關,或是真空管。然而這個觀念在高階的程式語言中已慢慢被抹滅,我們可以「字串」的角度而非字元的角度看待資料,可以「物件」而非函式的角度來處理資訊。Ruby 內建很多新的物件供使用者運用,但如何「看山還是山,看水還是水」,我們首先需要一些物件導向的觀念來輔助學習。
物件導向
古早的高階語言中,沒有物件的概念,只有「程序」及「資料」,將一堆資料整理成有用的資訊,便是程序的工作了。然而程式寫久了會日漸複雜,就像我們總是會將房間的書桌堆滿雜物一樣,一旦我們需要在裡面找出一張紙,可能就會面臨大海撈針的困擾以及山崩的危險。不過幸好人類不愧為靈長類動物中腦袋最皺的種族之一,程序慢慢演化成了可以依參數不同而處理多項工作的「函式」,最後函式與資料合而為一,成為了「物件」,透過這種設計模式 ( Design Pattern ),程式碼的重複使用性變高了,也讓程式設計師們擁有更多的時間去整理他們的書桌。
物件導向概念在這邊我們就不贅述了,只就對於 Ruby 而言,該怎麼使用物件來陳述。
函式 ( Functions )
跟一般程式語言相同,在 Ruby 裡面我們也可以副程式,用以減少重複的程式碼:
def sing_a_song # define a "sing_a_song" subroutine puts 'La Wryyyyy' end puts 'I sing a song for you:' sing_a_song puts 'And another song:' sing_a_song puts 'They are different songs, I swear!'
# (Result) # I sing a song for you: # La Wryyyyy # And another song: # La Wryyyyy # They are different songs, I swear!
要注意到的是 Ruby 沒有所謂的主程式,任何在區塊之外的程式碼都會在讀取時被執行( 所謂區塊包括定義函式、物件等等 )。
定義函式當然也是可以的:
def say(something='Baa~')
puts "I'm glad to say: " + something
end
say "Hello, Ruby!"
say "Hello, Taiwan!"
say
# (Result)
# I'm glad to say: Hello, Ruby!
# I'm glad to say: Hello, Taiwan!
# I'm glad to say: Baa~
不過這些在 Ruby 不叫做 procedure 或 functions,而是叫做 Method,至於為什麼,這個我們要留到後面一點討論。
類別與物件 ( Classes & Objects )
我們知道物件是一堆屬性與方法 ( Properties & Methods ) 的結合,在 Ruby 中,要令一個 Class 產生實體的方法如下:
File.new('existed_file') # 新增一個檔案物件
String.new # 新增一個空白字串物件
跟一般物件導向程式語言不同的是,Ruby 改用「呼叫 new 方法」來取代「使用 new 陳述式」。接下來建立一個簡單的物件試試看:
class Human
def initialize
puts 'A child was born.'
end
end
Human.new
# (Result)
# A child was born.
Ruby 裡物件的建構子一律稱做 initialize,其餘定義方式皆與一般 method 相同。不過令人在意的是,既然 Ruby 的變數不用宣告,那該怎麼識別一個變數是否為實體變數 ( instance variable ) 呢?來看下一個例子:
class Human
def initialize
name = 'this is local variable'
@name = 'noname'
end
def get_name # accessor
return @name
end
def set_name(name) # mutator
@name = name # Note: the 'name' here is local variable
end
end
puts Human.new.get_name
# (Result)
# noname
物件的實體變數皆為 @ 開頭,所以不用擔心宣告的問題,不過要注意的是,在 Ruby 裡面我們習慣將 accessor 設定為跟該屬性同名的函式:
class Human
def initialize
@name = 'noname'
end
def name # accessor
return @name
end
def name=(name) # mutator
@name = name
end
end
human = Human.new
human.name = 'gotname'
puts human.name
# (Result)
# gotname
沒有定義這兩個存取 method 的話,實體變數預設會是 private 的,這點要特別注意。但是這樣一來每個 property 都要寫存許 method 實在太麻煩,於是貼心的 Ruby 允許使用者這樣寫:
class Human
attr_accessor :name # read and write
attr_reader :age # read only
attr_writer :mailbox # write only
def initialize
@name = 'noname'
@age = 0
@mailbox = []
end
end
也就是以 attr_accessor 代替 accessor 與 mutator 的建構,很方便的。至於 “:name” 是什麼東西,可以視他為一個比較不耗系統資源的 String,它的名子叫 Symbol,留待下一章再介紹。冒號與單字間沒有空白,注意可別寫成:
attr_accessor: name
了呀!
繼承
所謂物件導向程式設計的三大特性,指的是「封裝(encapsulation)」、「繼承(inheritance)」、「多型(polymorphism)」,封裝是為了保護物件內部的部份資料能不被存取,繼承則是在下認為最能體現物件導向程式優點的一大特性,因為它可以大幅增加程式碼的重複使用性,減少冗餘、重複的贅碼──即使很多物件導向的書籍不建議我們過度地使用繼承。
class Human
def initialize
puts 'A normal human was born!'
end
def sleep
puts 'I am sleeping...'
end
end
# Note the "less than" symbol represents "extends"
class Brainless < Human
def initialize
puts 'Baa~ I has no brain.'
end
end
human = Brainless.new
human.sleep
# (Result)
# Baa~ I has no brain.
# I am sleeping...
人類也有三大特性:「封裝(dressing)」、「繼承(inheritance)」、「多行(iamreallyuseful)」,整天把自己打扮得光鮮亮麗,整天等待親屬遺產,以及整天誇大自己有多行。不過顯然程式的世界裡這些困擾比較少,人只要會睡覺就可以了。不過人類分很多種,其中有一種是沒有大腦的人類,我們稱之為 Brainless。雖然只有人類被定義為會睡,但繼承的結果讓無腦人也能安然入眠,實為一大福音。
用 Ruby 實作多型
假如要寫一個函式,讓不管哪種人當作參數傳入都能入睡,應該會長成這個樣子:
def force_sleep(human)
human.sleep
end
然而程式設計師腦內 built-in 的 API 多不勝數,要是一不小心忘記,傳了一頭 Cow 進去,就不知道該怎麼讓牠睡了,這時候我們希望能對這個參數做檢查,在 Java 中,我們可以在 Compile 階段強制傳入的參數為 Human:
public void forceSleep(Human human)
// ...
end
但是 Ruby 沒有辦法宣告變數,所以也沒辦法在 Compile 階段強制規定傳入的參數,因此我們可以用下列方式代替:
def force_sleep(human)
if human.is_a?(Human)
human.sleep
else
puts 'Sorry, it is still awake!'
# or you can throw some exceptions
end
end
相當麻煩,Ruby 在實作多型上有這樣的弱點,期望它以後能夠改進。
Posted by Ruby@Taiwan on Friday, August 10, 2007