第三章:方法與物件 ( 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