Chapter 2: The Object Model - Part I

Open Class 開放類別

Open class 開放類別是 Ruby 程式語言的特色之一,所謂的開放類別就是讓 Ruby 內部的類別可以增加,甚至是改寫原有的方法。以下面的範例來說:

class Dog   
  def jump
    'jump'
  end
end

class Dog
  def run
    'run'
  end
end

dog = Dog.new
dog.jump         # => "jump"
dob.run         # => "run"

當 Ruby 第一次遇到 class Dog 時,發現 class Dog 並不存在,所以會定義此類別及 jump()方法。接者第二次再遇到 class Dog 時,會知道 class Dog 已經存在於 Ruby 了,因此會 “打開” 類別,讓 run()方法可以進入而類別 Dog 就會多了 run() 方法了。

當然你也可以改寫 Ruby 內建類別,例如:String 、Array 等…

grade = [90, 88, 4]
grade.size       # => 3, 還未改寫內部size方法前

class Array
  def size
    10               # => 只要是呼叫size方法就會回傳10
  end
end

grade.size         # => 10, 改寫size方法後

The Problem with Open Classes

” The term monkey patch only refers to dynamic modifications of a class or module at runtime, motivated by the intent to patch existing third-party code as a workaround to a bug or feature which does not act as desired. “ ― Wikipedia ―

Monkey Patching

開放類別常見的問題就是在複雜的專案中,如果沒有適當的控管,有可能會修改到相同名字方法,而造成與之前撰寫好的程式碼產生衝突。當我們只是為了要解決程式裡的某個 bug 或是新的功能,而直接去利用開放類別的便利性去做修改,就是所謂的 “猴子補丁 (monkey patching)” 。

Ruby 有提供可以檢查是否有重複使用名稱的方法

[ ].methods.grep /^re/  # => 利用 regexp 找出所有re開頭的方法名稱

更詳細的介紹可以參考:

Inside the Object Model

“Where you learn surprising facts about objects, classes, and constants.” ― Metaprogramming Ruby ―

What is in an Object ?

class MyClass
  def my_method
    @v = 1
  end
end

obj = MyClass.new
obj.class                 # => MyClass

每個實例物件都會有它的實例變數,加上ㄧ個指向屬於CLASS的連結。

   OBJECT = INSTANCE VARIABLES + a REFERENCE to a CLASS 

以下面的範例來說:這裡的 obj 是 MyClass 以 new 方法實例化的一個實例物件,而它的實例變數是 @v 以及指向 MyClass 的ㄧ個連結。 https://ithelp.ithome.com.tw/upload/images/20200919/20120868HPtCEGVu8p.png

Instance Variables

在 Ruby 的世界裡,實例變數(instance variable)是住在實例物件(instance object)裡面,而方法(method)卻是住在類別(class)。延續先前上面的範例,可用以下方法從實例物件來查詢它的實例變數:

obj.my_method
obj.instance_variables       # => [:@v]

有注意到是先呼叫 my_method() 而不直接呼叫 instance_variables 查詢實例變數。 原因是在 Ruby 程式設計裡,為了要讓都是屬於同一個 class 的 objects 都可以擁有自己不同的實例變數。 所以設計上物件(object)本身的類別(class)跟實例變數(instance variable)脫鉤。因此需要先呼叫 my_method() 方法 才能讓 @v=1 被定義。

class MyClass
  def my_method
    @v = 1
  end

  def my_name(name='default')
    @name = name
  end
end

obj1 = MyClass.new
obj2 = MyClass.new
obj1.class                     # => MyClass

obj1.instance_variables        # => [], 實例變數還未定義

obj1.my_method                 # => 1, 呼叫my_method後,實例變數就被定義

obj1.instance_variables        # => [:@v]

obj1.my_name('Jean')           # => Jean

obj2.my_name                   # => default

obj2.my_name('Kevin')          # => Kevin

Methods

Objects 物件除了有實例變數外,也擁有在 class 內的許多方法可以使用。書中有介紹可查詢物件所擁有的方法:

obj1.class.superclass     # => Object

# 因為obj1從'Object'繼承了許多方法,所以回傳一堆obj1可使用的方法,
obj1.methods

# 利用regexp找出所有my開頭的方法
obj1.methods.grep(/my/)              # => [:my_method, :my_name]

# 可以找出MyClass的實例方法, 帶入false參數是要過濾掉從'Object'繼承的方法
obj1.class.instance_methods(false)   # => [:my_method, :my_name]

” An object’s instance variables live in the object itself, and an object’s methods live in the object’s class. “― Metaprogramming Ruby ―

注意:雖然 實例方法 instance methods 是需要實例化的物件(例如: obj1)才能使用,但是實例方法是住在類別(例如: MyClass)裡面

The Truth About Classes

” Here is possibly the most important thing you’ll never learn about the Ruby object model: classes themselves are nothing but objects.”― Metaprogramming Ruby ―

在 Ruby Object model 裡,類別 (Class) 其實也是實例化的物件(Object) 。

"Kevin".class                # => String Class
String.class                 # => Class

先前提過的物件 (Object) 特性: 實例變數是住在實例物件裡面,而方法卻是住在類別

實例物件 (instance object) 的方法是定義在類別 (Class) 裡,而 Class 也是實例化的物件。因此 這些方法也是該類別 (Class) 的實例方法 (instance methods)。

"Kevin".class                  # => String Class
String.class                   # => Class
Class.class                    # => Class
Class.instance_methods(false)  # => [:allocate, :superclass, :new]

從 Class.instance_methods(false) 可得知屬於類別自己而不是繼承來的方法為:allocate、superclass、 new 。其中最常見的就是 new 方法了,而 superclass 方法則是用來取得繼承的父類別

Module (模組)

也許讓很多人驚訝的是類別其實是繼承自模組,也就是說每個 class 也都是 module。

Class.superclass                   # => Modul 
Class.instance_methods(false)  # => [:allocate, :superclass, :new]

還記得剛剛提到的 3 個 Class 的實例方法:[:allocate, :superclass,:new],我們可以這樣說 Class 就是繼承自 Module 再加上 3 個以上實例方法。在許多的情況下,選擇使用 Class 還是 Module 的界線並不是那麼清楚。通常可以依照該程式碼區塊是否有擴展、繼承或是有實例化的需求,來作為考量的基礎。

Constant (常數)

在 Ruby 的世界中,類別與模組都必須以常數命名,其規定也很簡單,就是必須以英文大寫開頭。 https://ithelp.ithome.com.tw/upload/images/20200920/20120868kOOTryCupC.png 嚴格來說,其實常數與變數的差別並不是很顯著,因為常數也是可以被修改的(只是會收到警告訊息而已!) 。兩者之間最重要的不同之處在於它們的作用範圍。由於 Ruby 編排所有常數的方式類似我們熟悉的檔案系統:類別或是模組比擬資料夾,而常數就相同於檔案。 https://ithelp.ithome.com.tw/upload/images/20200920/20120868LDxM4hQPp3.png 以上面程式碼來說,常數的編排方式會如下圖: https://ithelp.ithome.com.tw/upload/images/20200920/20120868lAp3oMXA9z.png 也就是說即使影片名稱都ㄧ樣命名為 Favorite, 如果我要看美國的限制級戰警,點選在USA資料夾內的 Favorite 影片才是正確的, 如果點到JP資料夾內的 Favorite 影片檔,就會是別的影片了。在 Ruby 中常數也是如此,要得到 “XXX” 在類別 JP 就必須得用『路徑』來區分並取得 Favorite 常數的值。

在 irb 裡便可看出需要以標示路徑 MyMovie::USA::Favorite 來取得正確的值。

重點回顧

  • Open Class 增加了在撰寫程式的靈活性,也是許多人喜愛 Ruby的特色之一。只要使用得當,會讓程式閱讀上更好,日後在維護程式也將更有效率。
  • 每個物件都有屬於它的實例變數,加上ㄧ個指向屬於CLASS的連結
  • 實例變數是住在實例物件裡,方法是住在類別
  • 類別也是物件
  • 類別繼承自模組並擁有 3 個實例方法:new()、superclass()、allocate()
  • 英文字母大寫開頭都是常數
  • 常數是以自己獨特的路徑來區分及取值