PJCHENder 未整理筆記

[Ruby] 物件(object)、類別(class)和模組(module)

2017-11-01

[Ruby] 物件(object)、類別(class)和模組(module)

@(Ruby on Rails)[Ruby]

透過類別(class)我們可以組織和產生許多具有相似屬性(attributes)和方法(methods)的物件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# CheatSheet
class Customer

attr_reader :id, :name
attr_writer :id, :name
attr_accessor :id, :name

def initialize(id, name)
@id = id
@name = name
end

def self.greeting
end

private
protected

end

類別(class)

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

定義類別

  • 使用關鍵字 class 來建立類別
  • 透過關鍵字 initialize 來建立初始化物件的方法
  • @ 開頭的方式建立「實例變數instance variable」,每一個透過這個 class 建立的物件都會有自己的實例變數
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class [ClassName]
# 在這裡定義 class variable
@@class_variable = [class_vairable]

# 在 initialize 中的程式會在 instantiation 的過程先被執行
def initialize([arg1], [arg2], ...)
# 一般來說 arguments 和 instance variable 的名稱會一樣
@arg1 = arg1
@arg2 = arg2
end

# Do something here ...

# instance method with instance variable,建立物件時就要放入參數
def get_custom_info
"id: #{@id}, name: #{@name}, addr: #{@addr}"
end

# instance method without instance variable,可以在建立物件後再放入參數
def greeting(name)
"Hello, #{name}. Welcome to our store"
end

end

透過類別建立物件

使用 Class.new('argument') 的方式來建立物件

1
2
3
4
5
6
7
class Person
def initialize(name)
@name = name
end
end

matz = Person.new('Yukihiro')

類別中的方法(methods in class)

類別中的方法主要可以分成實例方法(instance methods)類別方法(class methods):

  • 實例方法(instance method):能夠在實例上被使用的方法,大部分類別中的方法都是屬於實例方法。
  • 類別方法(class method):能夠在類別(class)上被使用的方法

實例方法(instance method)

  • 在 class 中直接建立的 method 就是 instance method
  • 它可以被該 class 所建立的 instance 所使用,亦可代入參數
1
2
3
4
5
6
7
8
9
10
11
class Sample
# 這是實例方法(instance method)
def hello(name)
puts "Hello #{name}"
end
end

# use class to create objects with the method
instance = Sample.new # create a object with "new"
puts instance.methods # list all the method in the class
instance.hello "Kitty" # 這個 hello method,直接作為於實體 instance 上,稱作 instance method。

類別方法(class method)

  • class method 指的是可以作用在 class 上的 method
1
2
3
4
5
6
# 這裡 .all 是作用在 Post class 上,所以稱作 class method
class PostsController < ApplicationController
def index
@posts = Post.all # all 是一個 class method
end
end

使用關鍵字 self 建立 class method:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
###
# 透過 self 可以建立屬於該 class 的 method(class method)
# self 指的就是該物件本身(Hello)
# self.sayhi_class 等同於 Hello.sayhi_class
###

class Hello

def self.sayhi_class(name)
return "Hello, #{name}"
end

def sayhi_instance(name)
return "Hello, #{name}"
end
end

使用類別方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
###
# :sayhi_class 直接作用在 Hello class 上,是 class method
# :sayhi_instance 作用在 Hello class 所建立的 greet instance 上,是 instance method
###

puts Hello.sayhi_class("Aaron") # class method
greet = Hello.new
puts greet.sayhi_instance("Aaron") # instance method

p greet.respond_to?(:sayhi_instance) # true,greet 中有方法 sayhi_instance(檢查該物件是否包含該方法)
p greet.respond_to?(:sayhi_class) # false,greet 中沒有方法 sayhi_class
p greet.class # Hello,greet 的 class 是 Hello
p greet.class.respond_to?(:sayhi_instance) # false,Hello 中沒有 sayhi_instance
p greet.class.respond_to?(:sayhi_class) # True,Hello 中有 sayhi_class

# puts greet.method(:sayhi_instance) # 檢查該物件的方法來自哪裡

保護所定義的 instance method

在 Ruby 中通常會在最前面加上 # 來表示它是個 instance method,有三種層度可以保護 instance method(對於 instance variable 或 class 則沒有保護)

  • Public Method:可以在程式中被呼叫(類似公開的 API),除了 initialize 之外,其他的 methods 預設都是 public。
  • Private Method:在 class 的外面無法被存取,只有在 class 內可以提取 private 的方法。
  • Protected Method:只能被所定義的 class 和它的 subclass 所呼叫。

使用方法

我們可以使用關鍵字 privateprotected 來保護所定義的方法:

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
###
# 第一種寫法:
# 在最後定義哪些是 private 或 protected
###
private :[method_name1], :[method_name2]
protected :[method_name1], :[method_name2]

###
# 第二種寫法:
# 在 private 或 protected 關鍵字後的都是某類型的 instance method
###

class ClassName
def initializa(param)
@param = param
end

public
def public_method; end

private
def private_method; end

protected
def protected_method; end
end

範例

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
27
28
29
class Box
def initialize(w,h)
@width = w
@height = h
end

# instance method 預設是 public
def getArea
getWidth * getHeight
end

def getWidth
@width
end
def getHeight
@height
end
# 透過關鍵字 'private' 設定方法為 private 狀態
private :getWidth, :getHeight


def printArea
@area = getWidth * getHeight
puts "Big box area is: #@area"
end

# 透過關鍵字 'protected' 設定方法為 private 狀態
protected :printArea
end

使用上述方法的結果:

1
2
3
4
block_area = Box.new(200,300)
block_area.getArea # 60000
block_area.getWidth # private method `getWidth'
block_area.printArea # protected method `printArea'

setter/getter: 取得或設定 class 中的 instance variable

因為 Ruby 並沒有「屬性」(property/attribute)這樣的東西,要取用實體變數,需要另外定義的方法才行:

getter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#######################################################
# 建立取得 instance variable 的方法 ##
# accessor/getter method ##
#######################################################
class Customer

def initialize(id, name)
@id = id
@name = name
end

# 建立取得 instance variable 的方法
# 方法的名稱通常會和 arguments, instance variable 一樣。
# 可以使用 attr_reader 快速取得
def id
@id
end

def name
@name
end

end

attr_reader

為了避免寫上面落落長一大段,我們可以使用 attr_reader 來快速設定 getter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
##
# 透過 attr_reader 可以快速建立取得 instance variable 的方法
# 不一定要放在 initialize method 前面
##

class Customer

attr_reader :id, :name

def initialize(id, name)
@id = id
@name = name
end

end

# 取的 instance variable 的值
aaron = Customer.new(1, "Aaron", "Tainan")
p aaron.id # 1
p aaron.name # "Aaron"

setter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Customer
attr_reader :id, :name, :addr

def initialize(id, name, addr)
@id = id
@name = name
@addr = addr
end

# 更改/設定 instance variable 的方法
def id=(value)
@id = value
end

def name=(value)
@name = value
end

def addr=(value)
@addr = value
end
end

attr_writer

為了避免寫上面落落長一大段的 setter,我們可以利用attr_writer快速設定 instance variable:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Customer
attr_reader :id, :name, :addr # 透過 attr_reader 可以快速取得 instance variable
attr_writer :id, :name, :addr # 透過 attr_writer 可以快速設定 instance variable

def initialize(id, name, addr)
@id = id
@name = name
@addr = addr
end

end

aaron = Customer.new(1, "Aaron", "Tainan")
p aaron.id # 1
aaron.id = 2
p aaron.id # 2

attr_accessor:同時建立 setter 和 getter

我們可以使用 attr_accessor 同時建立變數的 setter 和 getter:

1
2
3
4
5
6
7
8
9
10
class Customer

attr_accessor :id, :name, :addr # 等於同時寫了 attr_reade 和 attr_writer
def initialize(id, name, addr)
@id = id
@name = name
@addr = addr
end

end

類別的融合(開放類別)

在 Ruby 裡,如果遇到兩個一樣名字的類別,並不會「覆蓋」,而是會進行「融合」,因此如果我們不小心定義了相同名稱的類別:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Cat
def abc
# ...
end
end

class Cat
def xyz
# ...
end
end

kitty = Cat.new
kitty.abc # => 會發生什麼事?
kitty.xyz # => 會發生什麼事?

實際上的結果會是這樣:

1
2
3
4
5
6
7
8
9
class Cat
def abc
# ...
end

def xyz
# ...
end
end

為原本的類別添加方法

也因此,利用這個特性,可以為原本的 String class 添加方法,這個技巧稱之開放類別(Open Class)。:

1
2
3
4
5
6
7
8
class String
def say_hello
"hi, I am #{self}"
end
end

puts "eddie".say_hello # => hi, I am eddie
puts "kitty".say_hello # => hi, I am kitty

繼承(inheritance)

在物件導向的概念裡,通常會把相同功能的方法移到上一層的類別裡,然後再去繼承它。

可以使用關鍵字 < 來表示「繼承自」,寫 Child < Parent 表示 child 繼承自 parent,這時 Child 這個 class 被稱作 subclass,Parent 這個 class 則被稱作 parentsuperclass

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

# Cat 和 Dog 是不同 class,但都具有 eat method
class Cat < Animal
end

class Dog < Animal
end

在 Ruby 中任何的類別都只會有一個 superclass,因此不會有 multiple inheritance 的情況,下面這種 multiple inheritance 的情況會報錯:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Creature
def initialize(name)
@name = name
end
end

class Person
def initialize(name)
@name = name
end
end

##
# ERROR: superclass mismatch
##
class Dragon < Creature; end
class Dragon < Person; end

覆蓋(override)繼承來的方法

當 Dragon 繼承自 Creature 時,可以覆蓋掉 Creature 中原本的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Creature
def initialize(name)
@name = name
end

def fight
return "Punch to the chops!"
end
end

# Add your code below!

class Dragon < Creature
def fight
return "Breathes fire!"
end
end

使用 super 關鍵字來呼叫父層類別中的方法

我們可以使用 super([arg1, arg2, ...]) 來直接呼叫父層中同名的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Creature
def initialize(name)
@name = name
end

def fight(number)
return "Punch to the chops! #{number}"
end
end

class Dragon < Creature
def fight
puts "Instead of breathing fire..."
super(2) # 等於在這裡呼叫 Creature 中的 fight
end
end

dragon = Dragon.new('kuku')
dragon.fight

建立 .to_s 的實例方法

任何一個你所定義的 Class 都應該要有一個 to_s 的 instance method,目的是要以字串的形式回傳 object 的內容。

要留意的是,當我們在使用 puts 時,會自動代入 to_s 這個方法,因此在 class 中建立 to_s 方法會覆蓋掉原本的 to_s。

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
27
#################################
# The to_s method ##
#################################

class Customer

attr_accessor :id, :name, :addr # 等於同時寫了 attr_reade 和 attr_writer
def initialize(id, name, addr)
@id = id
@name = name
@addr = addr
end

def to_s
"This class contains (instance) variables include #{@id}, #{@name}, #{@addr}"
end

end

aaron = Customer.new(1, "Aaron", "Tainan")

##
# puts 會自動代入.to_s,因此會回傳
# "This class contains (instance) variables
# include 1, Aaron, Tainan"
##
puts aaron

其他

在 Class 裡面的 instance variable (@)應用在字串時,可以直接寫 #@

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
a = 30
p "The number is #{a}"

class People
def initialize(name, age)
@name = name
@age = age
end

def show_age
return "#@name is #@age"
end
end

aaron = People.new("Aaron", 28)
p aaron.show_age

物件(object)

在 Ruby 中幾乎所有東西都是物件,即使是 class 本身也是 Class 這個 class 的物件(但 block 不是)。

物件(object)是類別(class)的實例(instance),我們可以透過 <class>.new 這個關鍵字來建立實例物件(instance object),這個過程稱做 instantiation (instantiate a class)。

另外,我們可以透過 object.class 來檢視這個物件屬於哪個類別。

物件的輸出

可以用 pp, yaml, 或 inspect

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#########################
# 輸出物件 ##
#########################
require 'pp'
require 'yaml'

class User
attr_accessor :name, :age
end

user = User.new
user.name = "John Smith"
user.age = 30

puts user.to_yaml
puts pp(user)
1
2
3
4
5
6
7
#########################
# 輸出 JSON ##
#########################
<?
require 'json'
puts JSON.pretty_generate(JSON.parse(object.to_json))
?>

模組(Module)

觀念

你可以把模組想成是包含許多方法和常數的工具箱。模組其實和類別的概念很相似,但模組不能建構實例(instance),而且也不會有子類別(subclass),它只是用來儲存東西的。

在模組中不太會包含變數(variable),因為變數可能會變動。相反地,因為常數(constant)不會變動,所以我們可以把一些固定的常數存在模組中。

scope resolution operator

模組有一個很重要的功能就是將許多不同的方法和常數分入不同的命名空間(name space),稱作 namespacing,因此 Ruby 將不會無法區別 Math::PICircle::PI

這裡的 :: 稱作 scope resolution operator,它告訴 Ruby 可以在哪裡去找到對應的程式碼,例如,Math::PI 表示在 Math module 裡的方法 PI

建立模組

我們可以透過關鍵字 module 來建立模組,命名規則與 class 相同,使用駝峰大寫

1
2
3
4
5
6
7
8
9
10
11
## 透過 module 建立模組
module Flyable
FAVEROITE_DRINK = 'Spring tea'
def self.feature
puts "Fly"
end

def fly
puts "I can fly!"
end
end

引用模組到環境中

有些內建的模組並沒有預設載入到環境中,因此當我們想要使用這個模組時,我們可以使用關鍵字 require,記得引入的模組名稱要加上分號:

1
2
# require 'module'
require 'data'

引用模組到類別中

如果我們想要把 module 引用到 class 中時,我們可以使用關鍵字 includeextend

include

在 class 中當我們想要載入模組時,使用關鍵字 include,就可以讓這個 class 的實例擁有該 module 所定義的方法:

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
27
28

class Dog

##
# 透過 include 在類別中掛上模組
##
include Flyable

def initialize(name)
@name = name
end

def bark
puts 'Bark! Bark!'
end

def favorite_drink
puts FAVORITE_DRINK
end
end

little_yellow = Dog.new('litter_yellow')

##
# 透過 include 掛入的實例方法
##
little_yellow.fly
little_yellow.favorite_drink

extend

透過 include 我們可以把 module 中的方法在實例中被使用;透過 extend 則是可以把 module 中的方法在類別中被使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
module ThePresent
def now
puts "It's #{Time.new.hour > 12 ? Time.new.hour - 12 : Time.new.hour}:#{Time.new.min} #{Time.new.hour > 12 ? 'PM' : 'AM'} (GMT)."
end
end

class TheHereAnd
extend ThePresent
end

##
# 透過 extend 掛入的類別方法
##
TheHereAnd.now

呼叫使用模組

1
2
3
4
5
6
7
8
9
10
11
12
##
# 在實例中使用 Module
##
little_yellow = Dog.new('litter_yellow')
little_yellow.bark
little_yellow.fly

##
# 直接使用 Module 的類別方法
##
puts Flyable.feature
puts Math::PI

程式碼範例

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
27
28
29
30
31
32
33
34
35
36
## 透過 module 建立模組
module Flyable
FAVORITE_DRINK = 'Spring tea'
def self.feature
puts "Fly"
end

def fly
puts "I can fly!"
end
end

class Dog

##
# 透過 include 在類別中掛上模組
##
include Flyable

def initialize(name)
@name = name
end

def bark
puts 'Bark! Bark!'
end

def favorite_drink
puts FAVORITE_DRINK
end
end

little_yellow = Dog.new('litter_yellow')
little_yellow.bark
little_yellow.fly
little_yellow.favorite_drink

混合(mixin)

當我們透過在 class 中加入 module,讓它可以擁有更多混合的行為和訊息時,我們把它稱作 mixin。透過 mixin 我們可以客制化 class 而不需要重寫程式碼。

參考資料

掃描二維條碼,分享此文章