定義類別,本身就是在進行抽象化,如果一個類別定義時不完整,有些狀態或行為必須留待子類別來具體實現,則它是個抽象類別(Abstract Class)。例如,在定義銀行帳戶時,你也許想將 一些帳戶的共同狀態與行為定義在父類別中:
# encoding: Big5
class Account
    def withdraw(amt)
        if amt >= @balance
            @balance -= amt
        else
            raise RuntimeError, "餘額不足"
        end
    end
    
    def to_s
        "
        id\t\t#{id}
        name\t\t#{name}
        balance\t\t#{balance}
        "        
    end
end顯然地,這個類別的定義不完整,id、name、balance沒有定義,嘗試使用這個類別進行操作時,就會發生直譯錯誤:
acct = Account.new
puts acct # ..in `to_s': undefined local variable or method `id' for..(NameError)
      
      
      puts acct # ..in `to_s': undefined local variable or method `id' for..(NameError)
你可以繼承這個類別來實作未完整的定義:
...
class CheckingAccount < Account
    attr_reader :id, :name, :balance
    def initialize(id, name)
        @id = id
        @name = name
        @balance = 0
        @overdraftlimit = 30000
    end
    
    def withdraw(amt)
        if amt <= @balance + @overdraftlimit
            @balance -= amt
        else
            raise RuntimeError, "超出信用額度"
        end
    end
    
    def to_s
        super +  # 呼叫父類別 to_s 方法
        "Over limit\t#{@overdraftlimit}
        "
    end
end
acct = CheckingAccount.new("E1223", "Justin Lin")
puts acct
現在的問題是,實際上開發人員還是可以用Account.new實例化,也許你可以修改一下Account的定義:
class Account
def initialize
raise RuntimeError, "不能實例化抽象類別"
end
end
      
      def initialize
raise RuntimeError, "不能實例化抽象類別"
end
end
如此,嘗試使用Account.new實例化後,在初始化方法中就會引發錯誤(不過,實際上Account實例確實有產生了,但就這邊的需求來說,目的算已達到)。
像Ruby這類的動態語言,沒有Java的abstract或interface這種機制來規範一個類別所需實作的介面,遵循物件之間的協定基本上是開發 人員的自我約束(當然,還得有適當的說明文件)。如果你非得有個方式,強制實現某個公開協定,那該怎麼作?像上面一樣,藉由直譯錯誤是一種方式,實際上視你的需求而定(是否可實例化、子類別是否定義初始化方法等),還有許多模擬的方式。
舉個例子來說,你想要設計一個猜數字遊戲,猜數字遊戲的流程大致就是:
顯示訊息(歡迎)
隨 機產生數字
遊戲迴圈
顯示訊息(提示使用者輸入)
取得使用者輸入
比較是否猜中
顯示訊息(輸入正確與否)
      隨 機產生數字
遊戲迴圈
顯示訊息(提示使用者輸入)
取得使用者輸入
比較是否猜中
顯示訊息(輸入正確與否)
在描述流程輸廓時,並沒有提及如何顯示訊息、沒有提及如何取得使用者輸 入等具體的作法,只是歸納出一些共同的流程步驟:
# encoding: Big5
class GuessGame
    ABSTRACT_METHODS = %w[message guess] 
    def self.inherited(subclz)
        class << subclz
            alias original_new new
            def new
                ABSTRACT_METHODS.each { |mth|
                    unless self.instance_methods.include? mth.to_sym
                        raise RuntimeError, "抽象方法 #{mth} 尚未實作" 
                    end
                }
                original_new
            end
        end
    end
    def initialize
        raise RuntimeError, "不能實例化抽象類別"
    end
    def go
        message @welcome
        number = (rand * 10).to_i
        loop do
            gnumber = guess
            case gnumber <=> number
            when 1
                message @bigger
            when -1
                message @smaller
            when 0
                break
            end
        end
        message @correct
    end
end
現在GuessGame是個抽象類別,如果你嘗試實例化GuessGame:
game = GuessGame.new
      
      則會引發錯誤:
main.rb:5:in `initialize': 不能實例化抽象類別 (RuntimeError)
from main.rb:16:in `new'
from main.rb:16:in `<main>'
      
      from main.rb:16:in `new'
from main.rb:16:in `<main>'
如 果有子類別繼承自GuessGame,在執行子類別定義前,類別方法inherited就會被執行,並傳入子類別,此時重新定義子類別的new方法,檢查 子類別的實例方法定義中,是否 包括ABSTRACT_METHODS指定的方法,如果沒有,就引發例外,如此可讓開發人員知道,尚有抽象方法沒有實作。為了可以執行原本建構與初始物件 的流程,使用alias關鍵字,將原本子類別的new取了個別名original_new,在檢查子類別確實有實作指定的方法後,執行 original_new。
如果是個文字模式下的猜數字遊戲,可以將顯示訊息、取得使用者輸入等以文字模式下的具體作法實現出來。例如:
class ConsoleGame < GuessGame
    def initialize
        @welcome = "歡迎\n"
        @prompt = "輸入數字:"
        @correct = "猜中了\n"
        @bigger = "你猜的比較大\n"
        @smaller = "你猜的比較小\n"
    end
    
    def message(msg)
        print msg
    end
    
    def guess
        message @prompt
        gets.to_i
    end
end
game = ConsoleGame.new
game.go
如果子類別忘了實作某個方法,則該子類別仍被視為一個抽象類別,如果嘗試實例化抽象類別就會引發錯誤。例如若忘了實作message(),就會發生以下錯誤:
main.rb:13:in `block in new': 抽象方法 message 尚未實作 (RuntimeError)
from main.rb:11:in `each'
from main.rb:11:in `new'
from main.rb:57:in `<main>'
      
      from main.rb:11:in `each'
from main.rb:11:in `new'
from main.rb:57:in `<main>'
類似地,將來還會介紹到模組,如果你也想要模擬Java中interface的作用,則可以定義一個模組並在類別中include:
# encoding: Big5
module Flyer         # 模擬 Java 中的 interface
    ABSTRACT_METHODS = %w[fly]
    def self.included(clz)
        class << clz
            alias original_new new
            def new
                ABSTRACT_METHODS.each { |mth|
                    unless self.instance_methods.include? mth.to_sym
                        raise RuntimeError, "抽象方法 #{mth} 尚未實作" 
                    end
                }
                original_new
            end
        end
    end
end
class Bird; end
class Sparrow < Bird  # 繼承 Bird
    include Flyer     # 模擬 Java 中實作Flyer
    def fly
        puts "麻雀飛"
    end
end
s = Sparrow.new
s.fly
如 果有類別include了Flyer,在執行類別定義前,模組方法included就會被執行並傳入類別。

