Ruby 物件導向程式語言(Object Oriented Programming)

Nathan Lee
Change or Die!
Published in
25 min readNov 12, 2017

物件導向程式語言(Object Oriented Programming),簡稱OOP,是程式設計的範型(Programming Paradigm)的其中一種,另一種為程序性程式語言(Procedural Programming)。

物件導向程式語言(Object Oriented Programming),主要用於解決日益龐大的資料、程式碼及有複雜功能的程式在維護上所面臨的困難,避免掉僅修改一小部分內容卻引起連串error的漣漪效應(Ripple Effect)。

所以programmer需要透過OOP這程式設計範型來創建一個容器,以便於在修改或操作程式某些部分時不會影響整個程式;就是整個程式可以分解成許多物件(Object)且物件間能互動,便於維護也能避免發生牽一髮而動全身的情形。

進入OOP前,先介紹4個專業術語(Terminology):

抽象化(Abstraction):

把真實世界的事物,轉化成物件的概念。必須專注在事物的本質上,把最高度相關的資料抽取出來,定義成為物件的屬性(attributes) 以及方法 (methods),並且忽略不相關的、不重要的細節。[節錄自Alpha camp lighthouse教材]

封裝(Encapsulation):

隱藏特定方法(method)的具體實作(Implementation)或執行步驟,把實作包起來。使外界僅能與物件的介面(Interface)溝通,不能干涉也無法看見實作,強迫物件的使用者專注於介面若要使用該方法。

  • 介面 (Interface):用以定義物件之外觀行為。
  • 實作 (Implementation):用以存放抽象化結果及描述如何達成外觀行為。

繼承(Inheritance):

子類別(subclass)會繼承父類別(superclass)的屬性和方法。使programmer能定義具有重複性的父類別,以及透過子類別來執行更細膩、更詳細的物件外觀行為。

多型(Polymorphism):

定義具有不同功能但是名稱完全相同的方法或屬性的類別。

  • 靜態多型/多載 (Static polymorphism/Overloading) :宣告相同的方法(method),但是傳入的參數/引數(argument)型態不同或是個數不同。
  • 動態多型/覆寫 (Dynamic polymorphism/Overriding) : 改寫從父類別繼承下來的方法(method),限制是回傳值型態跟參數(argument)型態及個數必須與父類別一樣。

Reference: Software Engineering — 物件導向設計:從需求轉程式 Object Oriented Design (OOD) : from Requirements to Code

什麼是物件?(What are objects?)

“In Ruby, everything is object”

物件(Object)由類別所產生出來。把類別(class)想像成模具,而物件(Object)就是由模具所刻印出來的實例(Instance)。

從同一個類別所產生出來的物件,均為各個獨立的物件,有著不同的屬性(state/attribute)或行為(behavior/method),也是各個不同的實例(instance)。

類別定義物件(Classes Define Objects)

Ruby在類別(class)中去定義了物件(Object)的屬性(Attributes)和行為(Behaviors)。例如物件的外在特質就好比為屬性(Attributes),而物件能做什麼就好比為行為/方法(Behaviors/methods)。

而定義類別的格式如下,

def 類別名稱
#...定義屬性、行為/方法
end

注意,類別名稱必須為常數,也就是說開頭必須是大寫英文字母,且若是由兩個或兩個以上英文單字組合而成,則必須使用大駝峰式命名法(Uper CamelCase)去命名,例如:Hello Kitty,要套在類別名稱上則變成HelloKitty。

順帶一提,Ruby的檔名則是用蛇形命名(Snake_Case),例如:hello_kitty.rb。

定義類別完,也定義了物件屬性跟行為後,則使用new這個class method去創建一個實例(instance),例如,

class HelloKitty
#...
end
kitty = HelloKitty.new

使用HelloKitty.new 從class HelloKitty產生一個實例(instance),並且將這個實例存於變數kitty中。並可視kittyHelloKitty類別的物件(Object)或者是實例(Instance)。

初始化(Initializing a New Object)

類別中的initialize method在每次創造新物件時都會被呼叫,透過new 這個class method去使用initialize這個instance method。在促成一個新的HelloKitty物件成為實例(instance)的初始化過程中去觸發這個initialize method並實作出新物件。

class HelloKitty
def initialize
puts "I'm Hello Kitty!"
end
end
kitty = HelloKitty.new # => I'm Hello Kitty!

實例變數(Instance Variables)

變數(Variable)前面加上@就成為了實體變數(instance variable),而實體變數僅存在每個物件內,用於將data與物件緊繫,一直到物件被銷毀(destroy)為止。

可以藉由new method將參數/引數(Argument)傳入initialize method中,作為實例變數(instance variable)的值(value),並作為物件的屬性資訊;所以藉由傳入引述的不同,每一個物件的屬性都是獨立的。

class HelloKitty
def initialize(name)
@name = name
end
end
kitty = HelloKitty.new("Kitty White")

上述例子中,新建一個名為kitty的物件/實例,物件屬性資料就存放在實例變數@name中,存放的資料內容為name,而name就是由new method傳入的引數(Argument)Kitty White,資料型態為字串(string)。

實例方法(Instance Methods)

所有出自相同類別的物件均有相同的行為(Behaviors)及不同的屬性(Attributes)。而物件的行為也就是實例方法(instance method)的展示,可以透過實例方法來展示屬性的資訊。

例如,

class HelloKitty
def initialize(name)
@name = name
end
def say_hi
puts "#{@name} says hi~!"
end
end
kitty = HelloKitty.new("Kitty White")
kitty.say_hi # => Kitty White says hi~!

上述例子,物件kitty的name屬性資訊內容可以透過say_hi這個instance method展現出來。

class HelloKitty
def initialize(name)
@name = name
end
def say_hi
puts "#{@name} says hi~!"
end
end
kitty = HelloKitty.new("Kitty White")
mimmy = HelloKitty.new("Mimmy White")
kitty.say_hi # => Kitty White says hi~!
mimmy.say_hi # => Mimmy White says hi~!

上述例子,物件kitty和物件mimmy均透過同一個instance methodsay_hi展現name屬性資訊內容,印證了出自相同類別的物件均可有相同的行為(Behaviors)及不同的屬性。

Accessor Methods

Ruby內建了幾個方法: attr_readerattr_writterattr_accessor

attr_reader 會幫忙產生getter method

attr_writer 會幫忙產生setter method

attr_accessor 會幫忙產生 getter & setter method

上述方法均以symbol作為參數,參數就是用來建立getter/setter method的方法名稱。

那為什麼會有Accessor method出現呢?目的就是取代徒法煉鋼的方式去設定getter & setter method,改用上述方法快速建立getter跟setter。

徒法煉鋼範例如下,

class HelloKitty
def initialize(name, age)
@name = name
@age = age
end
def age # 徒法煉鋼版本 getter method
return @age
end
def age=(new_age) # 徒法煉鋼版本 setter method
@age = new_age
end

def info
puts "#{@name} is #{@age} years old!"
end
end
kitty = HelloKitty.new("Kitty White", 18)
kitty.info # => Kitty White is 18 years old!
puts kitty.age # => 18
kitty.age=(43)
kitty.info # => Kitty White is 43 years old!

徒法煉鋼的版本中,一開使從類別HelloKitty創造出物件kitty,並將其物件屬性name屬性設為Kitty Whiteage屬性設為18,並透過執行info這個實例方法(instance method) kitty.info呈現出物件kitty的屬性資料內容Kitty White is 18 years old!

此時透過執行kitty.age方法取得物件kittyage屬性,得到18這個值,.age此時為getter method。

透過執行kitty.age=(43)方法設定物件kittyage屬性,把值43寫入,.age=(43)此時為setter method。

順帶一提,所以執行kitty.age一般來說會被視為在讀取kitty上的age屬性,事實上是在執行kitty.age()這個方法,小括號被省略了。

所以套用Accessor Method後,程式更簡便了,

class HelloKitty
attr_accessor :age
def initialize(name, age)
@name = name
@age = age
end
end
kitty = HelloKitty.new("Kitty White", 18)
puts kitty.age # => 18
kitty.age = 43
puts kitty.age # => 43

透過attr_accessor Ruby自動產生了名為age的getter method和名為age=的setter method。

此外以這個範例為例子,setter method的標準型態為.age=(43),由於Ruby能省略小括號所以可以寫成.age=43,且Ruby語法中在之間加了空白字元也是一樣.age = 43

Reference: Ruby 語法放大鏡之「attr_accessor 是幹嘛的?」

類別方法(Class Methods)

類別方法可以直接使用類別本身,而不用透過去創造任何一個物件後再使用,也就是不用依賴任何獨立的物件。

在定義類別方法時,會在類別方法名稱前面加上self.

...def self.what_am_i
puts "I'm a HelloKitty class!"
end
...

然後透過類別名稱使用,

HelloKitty.what_am_i         # => I'm a HelloKitty class!

一般而言,物件都具有屬性,假如我們所要使用的方法不需要任何物件屬性資料,我們可以直接套用類別方法。

類別變數(Class Variables)

實例變數是在變數前面加上一個@,而類別變數則是在變數前加上兩個@@。如下,

@ruby      # 實例變數
@@ruby # 類別變數

就像實例變數(instance variable)引用該指定物件的屬性資訊一樣,類別變數(class variable)則是使用整個類別的資源。

實例變數的使用範圍侷限於該物件內,只能在實例方法中被使用;而類別變數的使用範圍是整個類別,就是可以遊走在實體方法也可以遊走在類別方法中,而且除了該類別外,子類別(subclass)也可以共享類別變數。

簡言之,類別變數(class variable)跟著類別(class)走,實例變數(instance variable)跟著實例(instance)走。

類別變數(class variable)跟實例變數(instance variable)都可以被繼承,但只要是在類別(class)底下的任何一個實例改變了原本的類別變數(class variable)設定,其他出自同一個類別(class)的實例中的類別變數(class variable)也會跟著被改變。

舉個例子,

class HelloKitty
@@number_of_cats = 0
def initialize
@@number_of_cats += 1
end
def self.total_number_of_kitty_family
@@number_of_cats
end
end
puts HelloKitty.total_number_of_kitty_family # => 0
kitty = HelloKitty.new
mimmy = HelloKitty.new
georgy = HelloKitty.new
mary = HelloKitty.new
anthony = HelloKitty.new
margaret = HelloKitty.new
puts HelloKitty.total_number_of_kitty_family # => 6

@@number_of_cats 就是類別變數(class variable),初始值設為0。

只要使用new method初始化一個物件,則在initialize這個實例方法(instance method)中的類別變數(class variable)@@number_of_cats 就會+1

宣告self.total_number_of_kitty_family 這個類別方法(class method)來負責回傳類別變數(class variable)的值。

從範例輸出結果可以看出來類別變數@@number_of_cats 的值原本為0,在同一類別產生出hello kitty家族成員的6個獨立物件後,類別變數@@number_of_cats 的值變為6,因為在initialize這個實例方法中定義每一次初始化一個物件的時候類別變數@@number_of_cats 的值會遞增,新增了6個物件所以從0遞增為6。

如果新增一個子類別Friend,讓我們來看看類別變數@@number_of_cats 的值會怎麼變化,

class HelloKitty
@@number_of_cats = 0
def initialize
@@number_of_cats += 1
end
def self.total_number_of_kitty_family
@@number_of_cats
end
end
class Friend < HelloKitty
end
puts HelloKitty.total_number_of_kitty_family # => 0
kitty = HelloKitty.new
mimmy = HelloKitty.new
georgy = HelloKitty.new
mary = HelloKitty.new
anthony = HelloKitty.new
margaret = HelloKitty.new
puts HelloKitty.total_number_of_kitty_family # => 6
daniel = Friend.new
joey = Friend.new
puts HelloKitty.total_number_of_kitty_family # => 8

由子類別Friend初始化了兩個物件danieljoey後,類別變數@@number_of_cats 的值由6變為8,印證了子類別(subclass)也可以共享類別變數,所以類別變數的使用範圍是類別,而實例變數只能在同一個物件下被使用。

self

self指的就是物件(Object)本身。

所以使用/呼叫類別方法(class method)時,只要self.類別名稱就能呼叫。

例如,

class Example  def self.example
puts "class method"
end

def example
puts "instance method"
end
def examplelol
self.example
end
end
sample = Example.new
Example.example # => class method
sample.example # => instance method
sample.examplelol # => instance method

第一個Example.example是參考到類別(class)呼叫方法,selfExample這個class。

第二個sample.example是實例方法。

第三個sample.examplelol是實例方法,裡面的self是參考到實例(instance)sample本身。

常數(Constant)

類別內部會有一些不會更動的變數,這時即可將其定義成常數(Constant)。常數的格式是以大寫的英文字母開頭,但是大部分Ruby的programmer都會將整個常數名稱用大寫英文字母來表示。

例如:

MAXHP = 100      # 常數MAXHP,值為100,該值不會去更動。

繼承(Inheritance)

還記得在討論類別變數(class variable)時我用子類別(subclass)Friend初始化了兩個物件danieljoey 嗎?這個子類別(subclass)就是繼承父類別(superclass)HelloKitty

我們運用<符號來定義<左邊的類別繼承自<右邊的類別,左邊的類別即為子類別(subclass),而右邊的類別即為父類別(superclass)。如下,

class Friend < HelloKitty
end

左邊的類別名稱為Friend,為HelloKitty的子類別(subclass);右邊的類別名稱為HelloKitty,為Friend的父類別(HelloKitty)。Friend繼承自HelloKitty

而子類別(subclass)可以使用所有父類別(superclass)內定義的實例方法及類別方法。

舉個例子,如下,

class Example
def self.example
puts "class method"
end

def example
puts "instance method"
end
def examplelol
self.example
end
end
class Ex < Example
end
sample = Example.new
Example.example # => class method
sample.example # => instance method
sample.examplelol # => instance method
exx = Ex.new
Ex.example # => class method
exx.example # => instance method
exx.examplelol # => instance method

宣告一個類別Ex繼承自類別Example,並自類別Ex初始化一個物件exx,並且使用類別Example 內的所有方法,其中包含了類別方法與實例方法,輸出結果均與由父類別所產生的物件使用所有方法後的結果一致。

覆寫(Overridung)

繼承自父類別的方法是可以被更改的,例如下面範例,

class Example
def self.example
puts "class method"
end

def example
puts "instance method"
end
def examplelol
self.example
end
end
class Ex < Example
def self.example
puts "Overriding the class method"
end

def example
puts "Overriding the instance method"
end
end
sample = Example.new
Example.example # => class method
sample.example # => instance method
sample.examplelol # => instance method
exx = Ex.new
Ex.example # => Overriding the class method
exx.example # => Overriding the instance method
exx.examplelol # => Overriding the instance method

我們覆寫(Overriding)了在父類別Example中類別方法self.example和實例方法example ,因為Ruby會先確認初始化該物件(此例物件為exx)的類別(此例為類別Ex)中的方法,然後才順道檢視父類別(此例為類別Example)。

所以當Ruby執行exx.examplelol時,會先確認類別Ex中有沒有examplelol這個方法,但是事實上類別Ex中並沒有examplelol這個方法,所以Ruby會再去類別Ex的父類別Example中找尋該方法,然後使用該方法,所以即便類別Ex中沒有examplelol方法,最後還是能看到輸出結果Overriding the instance method

Super

super是Ruby內建的function,可以讓我們往繼承階級(inheritance hierarchy)去呼叫跟它最近的父類別中的同名method。

舉個例子,

class Example
def self.example
puts "class method"
end

def example
puts "instance method"
end
def examplelol
self.example
end
end
class Ex < Example
def self.example
print "subclass: "
super
end

def example
print "subclass: "
super
end
end
sample = Example.new
Example.example # => class method
sample.example # => instance method
sample.examplelol # => instance method
exx = Ex.new
Ex.example # => subclass: class method
exx.example # => subclass: instance method
exx.examplelol # => subclass: instance method

子類別Exself.exampleexample方法中使用了super function,因此除了覆寫的內容外Ruby還會去使用父類別中的self.exampleexample方法。

super常會使用在子類別initialize方法中,覆寫內容新增參數再加上super,透過super將父類別initialize方法中原有的參數引用到子類別initialize方法中。

然而,super function後面有加上小括號super()與沒有加上小括號super是有差異的。前者Ruby會自動將所有參數(Argument)都代進去來呼叫父類別的方法,後者則是自己指定參數。

舉個例子,

class HelloKitty
attr_accessor :name
def initialize(name)
@name = name
end
end
class Friend < HelloKitty
def initialize(age)
super
@age = age
end
end
daniel = Friend.new("43")
# => <Friend:0x0055df9cf597f0 @name="43", @age="43">

在上面例子中,在類別Friendinitialize方法中super沒有引用參數,進而使super function以其他方式被使用。 除了super function default的方法外,super function會自動地指向並轉傳(forward)所在方法中所引用的參數(Argument)。在範例中,super是在類別Friend中的initialize方法中被使用,所以沒有引用參數的super自然地就引用了類別Friendinitialize方法的參數age的值 ,並且將參數age的值傳到父類別HelloKitty中去使用,所以@name="43" 是這樣來的。

反之,

class HelloKitty
attr_accessor :name
def initialize(name)
@name = name
end
end
class Friend < HelloKitty
def initialize(name, age)
super(name)
@age = age
end
end
daniel = Friend.new("daniel","43")
# => <Friend:0x0055d71996d4d0 @name="daniel", @age="43">

這次super(name)就有引用參數,參數name就被傳回父類別並設定為實例變數@name的值。所以才會得到@name="daniel"@age="43"的結果。

模組(Modules)/Mix-in/Extend

Module跟類別(Class)非常相似,共同點是與類別相同可以在內部定義方法,而不同點是module不能透過new method來初始化物件。

命名方式也跟類別一樣,module名稱必須為常數,也就是說開頭必須是大寫英文字母,且若是由兩個或兩個以上英文單字組合而成,則必須使用大駝峰式命名法(Uper CamelCase)去命名。

而定義module的格式如下,

module 名稱
#...定義方法
end

module定好了後,如果要引用就要透過include這個方法。

例如,

module Performance
def speak
puts "I can speak"
end
end
class Cat
include Performance
end
class Dog
include Performance
end
kitty = Cat.new
kitty.speak # => I can speak
pluto = Dog.new
pluto.speak # => I can speak

module的使用時機,如果要做的功能可能在很多不的類別裡都會用得到,那可以考慮把功能寫在模組裡,需要的時候再透過include 方法讓透過類別(class)產生的實例(instance)直接繼承module中的method並直接使用,此舉就符合Ruby DRY的原則。

而透過module定義然後在不同類別使用include 方法引用即為Mix-in的概念,用Min-in的概念實現多重繼承。

一個 class只能繼承自另一個class;但在 class 中可以mix-in 許多 modules。

而Extend的例子如下,

module Performance
def speak
puts "I can speak"
end
end
class Cat
extend Performance
end
class Dog
extend Performance
end
Cat.speak # => I can speak
Dog.speak # => I can speak

當用extend方法來代替include時,extend則是直接讓類別(class)具有module裡的method,不用透過實例(instance)繼承module中的method再來使用。

Namespacing

namespacing 就是把類似的類別們( classes) 集合起來放進 module 裡。

使用 namespace 的優點:

  1. 在Ruby程式碼中可以很容易辨識相關的類別 classes
  2. 減少相同名稱的 classes 產生衝突

引用LunchShool中的例子,

module Mammal
class Dog
def speak(sound)
p "#{sound}"
end
end
class Cat
def say_name(name)
p "#{name}"
end
end
end

要使用module裡的類別時,就在module名稱後面加上::,再加上類別名稱,

buddy = Mammal::Dog.new
kitty = Mammal::Cat.new
buddy.speak('Arf!') => "Arf!"
kitty.say_name('kitty') => "kitty"

這樣就由module中的類別DogCat產生實例buddykitty,並且使用該類別中締役好的實例方法。

Reference: 類別(Class)與模組(Module)Ruby on Rails 實戰聖經[Ruby]include v.s extend以及require的差別Mixin / Extend / InheritanceObject Oriented Programming with Ruby by Launch SchoolRuby 筆記 (2)

--

--