程式扎記: [ Ruby Gossip ] Basic : 類別 - 抽象類別與介面

標籤

2014年11月12日 星期三

[ Ruby Gossip ] Basic : 類別 - 抽象類別與介面

Source From Here 
Preface 
定義類別,本身就是在進行抽象化,如果一個類別定義時不完整,有些狀態或行為必須留待子類別來具體實現,則它是個抽象類別(Abstract Class)。例如,在定義銀行帳戶時,你也許想將 一些帳戶的共同狀態與行為定義在父類別中: 
  1. class Account  
  2.     def withdraw(amt)  
  3.         if amt >= @balance  
  4.             @balance -= amt  
  5.         else  
  6.             raise RuntimeError, "餘額不足"  
  7.         end  
  8.     end  
  9.       
  10.     def to_s  
  11.         "  
  12.         id\t\t#{id}  
  13.         name\t\t#{name}  
  14.         balance\t\t#{balance}  
  15.         "          
  16.     end  
  17. end  
顯然地,這個類別的定義不完整,id、name、balance 沒有定義,嘗試使用這個類別進行操作時,就會發生直譯錯誤: 
 

你可以繼承這個類別來實作未完整的定義: 
  1. # encoding: utf-8  
  2. require "Account"  
  3.   
  4. class CheckingAccount < Account  
  5.     attr_reader :id, :name, :balance  
  6.     def initialize(id, name)  
  7.         @id = id  
  8.         @name = name  
  9.         @balance = 0  
  10.         @overdraftlimit = 30000  
  11.     end  
  12.   
  13.     def withdraw(amt)  
  14.         if amt <= @balance + @overdraftlimit  
  15.             @balance -= amt  
  16.         else  
  17.             raise RuntimeError, "超出信用額度"  
  18.         end  
  19.     end  
  20.   
  21.     def to_s  
  22.         super +  # 呼叫父類別 to_s 方法  
  23.         "Over limit\t#{@overdraftlimit}  
  24.         "  
  25.     end  
  26. end  
接著你可以如下使用: 
 

現在的問題是,實際上開發人員還是不小心會用 Account.new 實例化,也許你可以修改一下 Account 的定義: 
  1. class Account  
  2.     def initialize  
  3.         raise RuntimeError, "不能實例化抽象類別"  
  4.     end  
  5. end  
如此,嘗試使用 Account.new 實例化後,在初始化方法中就會引發錯誤: 
 

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

舉個例子來說,你想要設計一個猜數字遊戲,猜數字遊戲的流程大致就是: 
  1. 顯示訊息(歡迎)  
  2. 隨 機產生數字  
  3. 遊戲迴圈  
  4.     顯示訊息(提示使用者輸入)  
  5.     取得使用者輸入  
  6.     比較是否猜中  
  7.     顯示訊息(輸入正確與否)  
在描述流程輸廓時,並沒有提及如何顯示訊息、沒有提及如何取得使用者輸 入等具體的作法,只是歸納出一些共同的流程步驟: 
  1. # encoding: utf-8  
  2. class GuessGame  
  3.     ABSTRACT_METHODS = %w[message guess]  
  4.   
  5.     def self.inherited(subclz)  
  6.         class << subclz  
  7.             alias original_new new  
  8.             def new  
  9.                 ABSTRACT_METHODS.each { |mth|  
  10.                     unless self.instance_methods.include? mth.to_sym  
  11.                         raise RuntimeError, "抽象方法 #{mth} 尚未實作"  
  12.                     end  
  13.                 }  
  14.                 original_new  
  15.             end  
  16.         end  
  17.     end  
  18.   
  19.     def initialize  
  20.         raise RuntimeError, "不能實例化抽象類別"  
  21.     end  
  22.   
  23.     def go  
  24.         message @welcome  
  25.         number = (rand * 10).to_i  
  26.         loop do  
  27.             gnumber = guess  
  28.             case gnumber <=> number  
  29.             when 1  
  30.                 message @bigger  
  31.             when -1  
  32.                 message @smaller  
  33.             when 0  
  34.                 break  
  35.             end  
  36.         end  
  37.         message @correct  
  38.     end  
  39. end  
現在 GuessGame 是個抽象類別,如果你嘗試實例化 GuessGame 則會引發錯誤! 

如 果有子類別繼承自 GuessGame,在執行子類別定義前,類別方法 inherited 就會被執行,並傳入子類別,此時重新定義子類別的 new 方法,檢查 子類別 的實例方法定義中,是否 包括ABSTRACT_METHODS 指定的方法,如果沒有,就引發例外,如此可讓開發人員知道,尚有抽象方法沒有實作。為了可以執行原本建構與初始物件 的流程,使用 alias 關鍵字,將原本子類別的 new 取了個別名 original_new,在檢查子類別確實有實作指定的方法後,執行 original_new。 

如果是個文字模式下的猜數字遊戲,可以將顯示訊息、取得使用者輸入等以文字模式下的具體作法實現出來。例如: 
  1. # encoding: utf-8  
  2. require "GuessGame"  
  3.   
  4. class ConsoleGame < GuessGame  
  5.     def initialize  
  6.         @welcome = "歡迎\n"  
  7.         @prompt = "輸入數字:"  
  8.         @correct = "猜中了\n"  
  9.         @bigger = "你猜的比較大\n"  
  10.         @smaller = "你猜的比較小\n"  
  11.     end  
  12.   
  13.     def message(msg)  
  14.         print msg  
  15.     end  
  16.   
  17.     def guess  
  18.         message @prompt  
  19.         gets.to_i  
  20.     end  
  21. end  
  22.   
  23. game = ConsoleGame.new  
  24. game.go  
如果子類別忘了實作某個方法,則該子類別仍被視為一個抽象類別,如果嘗試實例化抽象類別就會引發錯誤。 

類似地,將來還會介紹到模組,如果你也想要模擬 Java 中 interface 的作用,則可以定義一個模組並在類別中 include: 
  1. # encoding: Big5  
  2. module Flyer         # 模擬 Java 中的 interface  
  3.     ABSTRACT_METHODS = %w[fly]  
  4.     def self.included(clz)  
  5.         class << clz  
  6.             alias original_new new  
  7.             def new  
  8.                 ABSTRACT_METHODS.each { |mth|  
  9.                     unless self.instance_methods.include? mth.to_sym  
  10.                         raise RuntimeError, "抽象方法 #{mth} 尚未實作"   
  11.                     end  
  12.                 }  
  13.                 original_new  
  14.             end  
  15.         end  
  16.     end  
  17. end  
  18.   
  19. class Bird; end  
  20.   
  21. class Sparrow < Bird  # 繼承 Bird  
  22.     include Flyer     # 模擬 Java 中實作Flyer  
  23.     def fly  
  24.         puts "麻雀飛"  
  25.     end  
  26. end  
  27.   
  28. s = Sparrow.new  
  29. s.fly  
如果有類別 include 了 Flyer,在執行類別定義前,模組方法 included 就會被執行並傳入類別。 

Supplement 
About.com - Ruby aliasing 
To alias a method or variable name in Ruby is to create a second name for the method or variable. Aliasing can be used either to provide more expressive options to the programmer using the class, or to help override methods and change the behavior of the class or object. Ruby provides this functionality with the alias and alias_method keywords...

Blog - alias vs alias_method

沒有留言:

張貼留言

網誌存檔