( 此系列文章用於記錄筆者在 Tealeaf Academy 學習 Rails 的心得和整理筆記,將不定期更新)

昨天剛上完 Tealeaf Academy Track 1 第二週的課程,這週的進度是完整介紹 OOP (Objected-Oriented Programming)的概念,目的是讓學員可以讀懂 Rails Code。

我非常瞭解新手看不懂 Rails code 的痛苦,想當初我在 Treehouse 學完了 HTML, CSS, RWD, jQuery 之後,原以為可以熱血衝一發把 Ruby 基礎學完後直衝 Rails ,沒想到我幾乎看不懂 Rails code,課程進度也一直卡關,即便把 Ruby 基礎課程複習了一遍又一遍,還是搞不懂每行 Rails code 的意義是什麼,那時候我一直覺得很奇怪,平平都是 Ruby 語言,為什麼 Rails 寫起來會差這麼多?

現在我懂了,最大的原因就是當時我 OOP 的觀念還沒有很熟,由於 Rails 是基於物件導向語言的特性發展出來的架構,若 OOP 基礎沒打好, Rails 對你來說會像天書一樣, 十行裡面有八行會看不懂…

第一週 Tealeaf Academy 刻意跳過 OOP 的觀念,先讓我們用醜醜的 Ruby code 做作業,而第二週學員必須利用所學到的 OOP 概念重新把第一週的作業再寫一次,順便複習基礎的 Ruby 知識。或許會有人好奇為什麼不一口氣教完,我想這是因為 OOP 的概念對新手來說很難理解,要新手一口氣學 Ruby syntax 和 OOP 有點強人所難,才會特意這樣安排。

依照慣例,筆者把自己覺得重要的觀念記了下來,希望能幫到學 Rails 的新手:

 

一、 Variable Types(變數類型)

在 Ruby 語言中,變數可分為五大類型,分別是 constants(常數), global variables(全域變數), class variables(類別變數), instance variables(實例變數) 和 local variables(區域變數)。

(1) Constants(常數)

當常數被宣告時,必須使用全大寫表示名稱:

FIRST_CONSTANT = "Hi, I'm a CONSTANT."

常數是用來儲存「不變資料」的變數。譬如說撲克牌的 21 點中 Blackjack 的值 ,或是猜拳遊戲中的手勢(剪刀、石頭和布),在整個遊戲(程式)的過程中都是不會變的,這些資料都很適合用常數的型態來儲存。

跟大部分程式語言不同, Ruby 允許我們改變常數的值,若我們直接改變常數「指向記憶體位址的值」( mutate the caller ),系統不會有警告,但如果我們重新指定常數「指向的記憶體位址」,不管值有沒有改變,系統都會給你警告。(感謝網友 Yulin 補充,關於此觀念可參考上一篇 Variable as pointer 的部分)

如以下所示:

CONSTANT_1 = [1, 2, 3]
CONSTANT_1.pop               #CONSTANT_1 = [1, 2]

CONSTANT_2 = [1, 2, 3]
CONSTANT_2 = [1, 2, 3]       #warning: already initialized constant CONSTANT_2

常數在整個程式中都能夠被使用( accessible )且常數不能在方法( method )中被宣告,但要注意若常數是在類別而不是在 global scope 被宣告的話,必須加上 Classname::CONSTANT 才能被使用

#Constants Example 

CONSTANT1 = 100              # 常數在 global scope 被宣告

puts CONSTANT1               # 可被直接使用

class Sample
  CONSTANT2 = 101            # 常數在 class 中被宣告
end

puts CONSTANT2               #這行會產生錯誤,因為系統找不到常數
puts Sample::CONSTANT2     #必須加上類別::常數才找得到

(2) Global Variables(全域變數)

宣告全域變數時,必須在名稱前面加上「$」符號:

$speed = 100

全域變數無視 variable scope ,可以在整個程式中被使用,由於全域變數能夠編寫於任何地方,容易造成重複性命名的問題,且全域變數違反了 OOP 的封裝(Encapsulation)特性,所以我們應該儘量避免使用全域變數。

(3) Local variables(區域變數)

區域變數是最常使用的變數,嚴格遵守 variable scope 的規範,宣告時不需要加「@」或者「$」等前綴符號:

var = "Hi, I strictly obey variable scope rule!"

(4) Class variables(類別變數)

宣告類別變數時,必須在名稱前面加上兩個「@」符號:

@@car_numbers = 4

類別變數儲存屬於類別層級的資料,類別變數可以被實例 (instance) 和類別 (class)所使用,一定要在 class level 被宣告,不能透過方法宣告,但可以透過類別方法(class method)或實例方法(instance method)改變其值。

(5) Instance variables(實例變數)

宣告實例變數時,必須在名稱前面加上「@」符號:

 @instance_variable = 2

實例變數用來儲存實例層級的資料,實例變數在同一個類別的實例中都可以被使用。

 

二、Class & Module  (類別與模組)

Class 和 Module 是 OOP 相當重要的觀念,兩者的概念有點相似,但本質上是完全不一樣的東西。

(1) Class (類別)

在 Ruby 中,Class 用來定義同一類物件的特性和行為。我們可以把 Class 想像成一個模子,這個模子決定了這個 Class 底下的物件會包含怎麼樣的「資料」( state )和可以做什麼「行為」( behavior ),所以透過這麼模子做出來的物件都會有類似的資料和行為。

現在我們來看一下實際的例子,假設我現在有很多台車,想要寫一個程式去儲存我車子的型號、年份和 cc 數,我可以創造一個叫做 MyCar 的類別,如以下所示:

class MyCar
  @@car_number = 0          # 初始車子數量 = 0 (類別變數)
   
   def initialize(m, y, c)
     @model = m             # 車子的型號 (實例變數)
     @year = y              # 車子的年份 (實例變數)
     @cc = c                # 車子的cc數 (實例變數)
     @@car_number += 1      # 每次新增車輛,車子數量就會+1
   end
   
   def say_model
     puts "Hello, this car's model is #{@model}!"
   end

   def self.total_number
     @@car_number
   end
end

在這個例子當中,我所有的車都屬於 MyCar 這個類別,我車子的數量用類別變數 @@car_number 來表示。而我的每一台車子都是 MyCar 類別底下的實例,這個類別決定了每輛車子都包含 @modle, @year, @cc 等資料( state ),而車子的行為( behavior )就是 say_model 這個方法,當我呼叫這個方法時,系統會告訴我這個車子的型號, self.total_number 是類別方法,若要定義類別方法要在前面加上 self. 的前綴詞,示意圖如下:

 

class1

@@car_number 預設值是 0 ,但當我每次新增一輛車的時候,數字就會增加 1 (  initialize 在每次新實例產生時都會被執行 ):

puts MyCar.total_number                  # 0

car1 = MyCar.new("Focus 5D", 1999, 2500)
car2 = MyCar.new("Lexus rx350", 2001, 1900)
car1.say_model                           # Hello, this car's model is Focus 5D!
car2.say_model                           # Hello, this car's model is Lexus rx350!

puts My_car.total_number                 # 2

 

(2) Module (模組)

Module 是一個對新手來說很容易混淆的概念,因為它可以儲存類別,也可以儲存方法,用法也很多樣,以下將一一介紹:

Module 可以做為實例方法的集合體,透過 Module ,我們可以讓類別的實例新增一些方法,在 Ruby 中我們使用「include」這個方法來把 Module 嵌入類別:

module WheelNumber
  def how_many_wheels? 
    puts "#{@@wheel_number}"
  end 
end

class MyCar
  @@wheel_number = 4
  include WheelNumber
end

class MyBike 
  @@wheel_number = 2
  include WheelNumber
end

MyCar.new.how_many_wheels?   # 4
MyBike.new.how_many_wheels?  # 2

除此之外,Module 還可以用來做為「命名空間」(name spacing),這邊的命名空間指的是我們可以把相似的類別放入同一個 Module,以防止類別有重複名稱。

這樣講有點抽象,讓我們直接舉個例子,假設我現在有兩個公司,一家是書店,一家是餐廳,兩家公司都有一樣的類別資料要儲存,像是員工資料等等,但這麼一來就麻煩了,我不可能同時有兩個 class 都叫做 Staff 吧? 但難道我每次命名類別的時候都要用 class BookStoreStaff 和 class RestaurantStaff 嗎? 這太麻煩了!!

這時候 Module 就派上用場了:

module BookStore
  class Staff
  end
end

module Restaurant
  class Staff
  end
end

Jack = BookStore::Staff.new
Benson = Restaurant::Staff.new

在這個例子中,雖然我們有兩個類別都叫做 Staff ,但因為分屬不同的 Module ,所以在 Ruby 中仍被視為不同的類別,巧妙的避開了重複命名的問題。

當我們要呼叫 Module 中的類別時,必須在類別名稱前加上兩個冒號( : : ),用 Module::Class 的型式呼叫。

最後,Module 也可以純粹當方法的集合體:

module Calculation
  ...

  def self.ten_fold(num)
    num * 10
  end
end

x = Calculation.ten_fold(10)              # 100
y = Calculation::ten_fold(50)             # 500

當我們要呼叫 Module 裡面的方法時,可以用 Module.method 或 Module::method 兩個方式都可以,不過會比較推薦使用第一個表現方式。

 

三、Public & Private & Protected

這三個方法是 Ruby 用來設定類別中方法( method )存取限制的工具,在預設狀況下,類別的所有的方法都是 public method,public method 不管是在類別內還是類別外都能使用,而 private method 只有在類別內能使用*,如以下所示:

(龍哥指正:這個說法事實上並不完全正確。正確的定義是 private method 只能在「無明確的 receiver」的狀況下被使用。但新手使用起來可以先這樣記,詳見龍哥 blog )

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

  def say_something            # public method
    puts "#{@name}: #{speak}"
  end

 private
  def speak                    # private method
    "woo~~"
  end
end

bob = Dog.new("Bob")           
bob.say_something              # "Bob : woo~~"
bob.speak                      # no method error

這個例子中我們呼叫了兩次 private method ,一次是在類別裡面的方法 say_something ,一次是在類別外被物件 bob 直接呼叫,後者會出現錯誤,因為 private method 不能在類別外被使用。

那麼,如果我們把 say_something 稍做修改,speak 改成 self.speak 會怎麼樣呢? :

def say_something            
  puts "#{@name}: #{self.speak}"
end

bob.say_something  # no method error

大家可能會以為 speak 方法還是可以在類別內被呼叫,但剛剛備註中有提到, 事實上 private method「只能在無明確的 receiver」的狀況下被使用,這邊的 self.speak 完全等於 bob.speak,這樣就有明確的 receiver 了,所以會發生錯誤,這是要特別注意的地方。

而 protected 的效果剛好介於 public 和 private 之間,規則如下:

  • 在類別之外, protected method 跟 private method 一樣。
  • 在類別以內, protected method 跟 public method 一樣。
class Sample
 def public_method
   puts "Will this work? " + self.protected_method 
 end

 def public_method2 
   puts "Will this work? #{protected_method}"      
 end 

 protected 
 def protected_method 
   "Yes, I'm protected!" 
 end 
end 

s = Sample.new     
s.public_method        #Will this work? Yes, I'm protected!
s.public_method2       #Will this work? Yes, I'm protected!
s.protected_method    # no method error

由以上例子可以看出,因為 protected method 在類別內會變成 public method,所以使用 self 呼叫就不會出現錯誤。

以上就是這週的筆記,下週的課程內容會用 Sinatra 作出線上的 Blackjack 遊戲,大家下次見囉~

cover photo via Unsplash