抽象類別與介面


定義類別,本身就是在進行抽象化,如果一個類別定義時不完整,有些狀態或行為必須留待子類別來具體實現,則它是個抽象類別(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)

你可以繼承這個類別來實作未完整的定義:
...
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

如此,嘗試使用Account.new實例化後,在初始化方法中就會引發錯誤(不過,實際上Account實例確實有產生了,但就這邊的需求來說,目的算已達到)。

像Ruby這類的動態語言,沒有Java的abstractinterface這種機制來規範一個類別所需實作的介面,遵循物件之間的協定基本上是開發 人員的自我約束(當然,還得有適當的說明文件)。如果你非得有個方式,強制實現某個公開協定,那該怎麼作?像上面一樣,藉由直譯錯誤是一種方式,實際上視你的需求而定(是否可實例化、子類別是否定義初始化方法等),還有許多模擬的方式。

舉個例子來說,你想要設計一個猜數字遊戲,猜數字遊戲的流程大致就是:
顯示訊息(歡迎)
隨 機產生數字
遊戲迴圈
   
顯示訊息(提示使用者輸入)
    取得使用者輸入
    比較是否猜中
   顯示訊息(輸入正確與否)

在描述流程輸廓時,並沒有提及如何顯示訊息、沒有提及如何取得使用者輸 入等具體的作法,只是歸納出一些共同的流程步驟
# 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>'


如 果有子類別繼承自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>'

類似地,將來還會介紹到模組,如果你也想要模擬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就會被執行並傳入類別。