上一篇文章簡單介紹物件導向 (OOP) 的設計模式,也稍微開個頭提到 Ruby 中的物件部分,有提到這句話:「Ruby 中的物件(Object)是一個類別(Class)所製造出來的實體(Instance)。」,也舉了例子表示物件由類別所產生出來。可以把類別想像成模具,而物件就是透過模具所刻印出來的實體。今天就來好好說明這個例子要表達的概念是甚麼,而類別又是甚麼吧。

類別 Class

類別中可以放置很多方法跟屬性,大家應該從前面對於物件導向的介紹中有看到,這些都寫在類別中。開始寫 rails 之後更是會注意到 Model 及 Controller 都是由 class 構築而成,且繼承自更上層的類別,這部分後面進入到 rails 的介紹會再有更多的說明。

定義類別

慣例上來說 class 會以大駝峰的方式來命名(CamelCase),例如 SayHello、PostsController、User。

1
2
3
class 類別的名字
#...
end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Human
def eat(food)
puts "#{food} 也太好吃了吧!!"
end
end

# Human類別中定義一個eat方法,可以代入food作為參數

Sean = Human.new # 建立實體
Sean.eat "咖哩" #=> 印出「咖哩也太好吃了吧!!」

Rex = Human.new
Rex.eat "麥當勞" #=> 印出「麥當勞也太好吃了吧!!」

# 用 Human 類別建立出 Sean 和 Rex 的實體,因為這兩個實體都是從 Human 中產生,所以都可以使用 eat 的方法

初始化 (initialize)

在使用 new 方法製作實體的時候,也可以順便傳參數進去,讓實體一建立就帶有相關的參數。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Human
def initialize(name, gender)
@name = name
@gender = gender
end

def say_hello
puts "hello, my name is #{@name}"
end
end

Sean = Human.new("Sean", "male")
Sean.say_hello # => hello, my name is Sean

如果要透過 new 方法傳參數進來,在類別裡面必須有個名為 initialize 的方法來接收傳進來的參數。在 initialize 方法裡,常見的手法是會把參數傳進來給內部的實體變數(instance variable)。

實體變數 instance variable

類別內會建立實體,就會使用到實體變數,用 @ 開頭的變數,是活在每個實體裡的變數,而且每個實體之間互不相影響。
在 Rails 專案中,實體變數應用到的頻率很高,最常用的地方應該是 Controller 與 View 之間,會透過實體變數去進行傳遞資料。

1
2
3
4
class UsersController < ApplicationController
def index
@users = User.all # 取得所有的user的資料,並放入@user這個實體變數
end

實體方法 (instance method)

1
2
3
4
5
class Human
def eat(food)
puts "#{food} 也太好吃了吧!!"
end
end

類別方法 (class method)

以前面使用過的 user controller 的例子來看

1
2
3
4
class UsersController < ApplicationController
def index
@users = User.all # 取得所有的user的資料,並放入@user這個實體變數
end

這裡的 all 方法是直接作用在 User 這個「類別」上,就稱為類別方法。在 Ruby 要定義類別方法有幾種寫法,其中一種比較簡單的,就是在前面加上 self:

1
2
3
4
class Human
def self.all
end
end

這樣就可以直接用 Human.all 的方式呼叫方法。下面是另一種寫法:

1
2
3
4
5
6
class Human
class << self
def all
end
end
end

這樣的寫法跟前面的一樣,但這樣就不需要特別在方法前面加上 self。當一個類別裡面有很多類別方法的時候,可以選擇這樣的寫法。

實體方法與類別方法比較

實體方法與類別方法的差異為作用的對象不同,實體方法是作用在實體上,而類別方法是作用在類別上。在 Ruby 中定義的方式也不同,定義類別方法則需要以 self 開頭或使用 class << self 裡面再定義方法,而實體方法則不用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Human
def initialize(name)
@name = name
end

def my_name
puts "I'm #{@name}."
end

def self.say_hi
puts "hi 我是類別方法"
end
end

Sean = Human.new('Sean')
麥克 = Human.new('麥克')

#實體方法 instance method
Sean.my_name # => I'm Sean.
麥克.my_name # => I'm 麥克.
Human.my_name # => 出錯(NoMethodError)

#類別方法 class method
Human.say_hi # => hi 我是類別方法
Sean.say_hi # => 出錯(NoMethodError),實體沒辦法使用
麥克.say_hi # => 出錯(NoMethodError),實體沒辦法使用

方法的類型與作用範圍

OOP 中的概念有提到一項是封裝的概念,主要可以利用一些私有方法來保護所定義的實體方法

  • public: 就是所有區域都可以直接存取。
  • private: 是只有在類別內部才可以存取。
  • protected: 介於上述這兩者之間,比 private 寬鬆一些,但又沒有 public 那麼自由,protected 在同一個類別內,或是繼承它的子類別可以自由取用,且可以有 recevier。

比較:

三種方法都可以在 class 內部被呼叫。
只有 public 方法能在外部被呼叫。
在內部呼叫 private 方法的時候不能夠有 receiver,而 protected 方法則沒有這種限制。

繼承(inheritance)

上面都提到單一類別,如果除了人類類別外還想要再加入一個小狗類別呢?,而且都有相同的方法,我們可以利用繼承的設計,可以把程式碼整理更簡單,且不會寫出一堆重複的程式碼。

1
2
3
4
5
class Human
def eat(food)
puts "#{food} 也太好吃了吧!!"
end
end
1
2
3
4
5
class Dog
def eat(food)
puts "#{food} 也太好吃了吧!!"
end
end

在這個例子,不管是 Human 或 Dog 類別都有定義了 eat 方法,在物件導向的概念裡,如果這些類別的用途是同一型的(例如 Human 跟 Dog 都算動物),通常會把相同功能的方法移到「上一層」的類別裡,然後再去繼承它。

1
2
3
4
5
6
7
8
9
10
11
12
class Animal
def eat(food)
puts "#{food} 也太好吃了吧!!"
end
end

class Human < Animal
end

class Dog < Animal
end

創造出 Animal 類別,然後讓 Human 跟 Dog 都去繼承它,應用小於符號 < 就是「繼承」。可以代表「Human 是一種 Animal, Dog 也是一種 Animal」。

類別的部分觀念真的比較複雜,還有許多觀念後續如果有更多時間會再慢慢補充給大家。


參考資料:

  1. 為你自己學 Ruby on Rails
  2. [Ruby] 物件(object)、類別(class)和模組(module)