Preface
定義類別,本身就是在進行抽象化,如果一個類別定義時不完整,有些狀態或行為必須留待子類別來具體實現,則它是個抽象類別(Abstract Class)。例如,在定義銀行帳戶時,你也許想將 一些帳戶的共同狀態與行為定義在父類別中:
- 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
你可以繼承這個類別來實作未完整的定義:
- # encoding: utf-8
- require "Account"
- 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
現在的問題是,實際上開發人員還是不小心會用 Account.new 實例化,也許你可以修改一下 Account 的定義:
- class Account
- def initialize
- raise RuntimeError, "不能實例化抽象類別"
- end
- end
像 Ruby 這類的動態語言,沒有 Java 的 abstract 或 interface 這種機制來規範一個類別所需實作的介面,遵循物件之間的協定基本上是開發 人員的自我約束(當然,還得有適當的說明文件)。如果你非得有個方式,強制實現某個公開協定,那該怎麼作?像上面一樣,藉由直譯錯誤是一種方式,實際上視你的需求而定(是否可實例化、子類別是否定義初始化方法等),還有許多模擬的方式。
舉個例子來說,你想要設計一個猜數字遊戲,猜數字遊戲的流程大致就是:
- 顯示訊息(歡迎)
- 隨 機產生數字
- 遊戲迴圈
- 顯示訊息(提示使用者輸入)
- 取得使用者輸入
- 比較是否猜中
- 顯示訊息(輸入正確與否)
- # encoding: utf-8
- 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,在執行子類別定義前,類別方法 inherited 就會被執行,並傳入子類別,此時重新定義子類別的 new 方法,檢查 子類別 的實例方法定義中,是否 包括ABSTRACT_METHODS 指定的方法,如果沒有,就引發例外,如此可讓開發人員知道,尚有抽象方法沒有實作。為了可以執行原本建構與初始物件 的流程,使用 alias 關鍵字,將原本子類別的 new 取了個別名 original_new,在檢查子類別確實有實作指定的方法後,執行 original_new。
如果是個文字模式下的猜數字遊戲,可以將顯示訊息、取得使用者輸入等以文字模式下的具體作法實現出來。例如:
- # encoding: utf-8
- require "GuessGame"
- 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
類似地,將來還會介紹到模組,如果你也想要模擬 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
Supplement
* About.com - Ruby aliasing
* Blog - alias vs alias_method
沒有留言:
張貼留言