iT邦幫忙

2021 iThome 鐵人賽

DAY 11
0
Modern Web

初階 Rails 工程師的養成系列 第 11

Day11. 活用 Ruby Class

  • 分享至 

  • xImage
  •  

Class 是Ruby很重要的觀念,要學習 Ruby 的一定要學會class & 物件。我們會在Day11-16 詳細講解何謂 class

以下為Day11會提及的內容

  • Constructor
  • Constructor with argument
  • getter & setter & attr_accessor
  • instance variable & instance method

Introduction

除了無法單獨存在的Block以外,所有的東西都是物件。

Ruby and Rails一段時間後發現,在Rails的專案中,我們只能在定義models, controllersclass裡面定義行為,不過大家有沒有想過,到底是哪個執行緒跑了這些class? Ruby on Rails 替我們做了很多事情,只要我們遵循慣例走就好。

寫一段時間Rails的朋友們,可能已經習以為常,但仔細想在專案裡面,下列的寫法有很多看點

  • 為什麼 Controller不用寫建構子?
  • <Rails 為繼承,那被繼承的 ApplicationController, ApplicationRecord 又是什麼?
  • Controller before_action 是如何被實作?
# Controller: controllers/admin/order_controller.rb
module Admin
  class OrdersController < ApplicationController
  end
end

# Model: models/order.rb
class Order < ApplicationRecord
end

這系列的文章可能沒有辦法帶到太多Rails的觀念,不過漢漢老師會不斷的在文章裡面提到自己使用Rails的一些經驗

constructor

建構子就是在初始化的時候給定一些變數,以及基本的設定。constructor,也有人叫做initializer,所以只要看到這兩個英文單字就是在講建構子。以Ruby程式語言來說的話,我們會在建構子初始化設定值在實體變數instance variable

以下方的車子為例,我們可以透過建構子指定車子的顏色、車子的大小。在 Ruby 中,建構子以initialize 表示。

class Car
  def initialize(color, size)
    @color, @size = color, size
  end
end

red_car = Car.new('red', 'big')    #=> #<Car:0x00007fd4ddc8ffa0 @color="red", @size="big">
blue_car = Car.new('blue', 'big')  #=> #<Car:0x00007fd4ddc8c3c8 @color="blue", @size="big">

我們可以想像到,當我們初始化一個物件red_car, blue_carCar就像是工廠,red_car, blue_cary則是被產出來的物件,而工廠就是Car類別,然而讀者也要清楚知道,Car也是被創造的物件。在 Ruby 語言當中,所有物件的源頭都在Basic Object

介紹一個方法superclass,透過superclass我們可以找到 Ruby 物件的源頭

Car.superclass
#=> Object
Car.superclass.superclass
#=> BasicObject
Car.superclass.superclass.superclass
#=> nil

⭐️我們可以在建構子做計算

class DataSet
  def initialize(ary)
    @ary = ary 
    @size = ary.is_a?(Array) ? ary.count : 0
  end
end

data_it = DataSet.new([1, 2, 3])    #=> #<DataSet:0x00007fd4ddcfd0f0 @ary=[1, 2, 3], @size=3>

default argument

我們可以在建構子設定參數的預設值。參數的預設值除了可以使用 || 來表示以外,還可以在括號裡面填入方法名後面的參數填入預設值

#=> default value with || operator
class Animal
  def initialize(*args)
    @species, @examples = args[0] || '猩猩', args[1] || '小龐'
  end
end

orangutan = Animal.new #=> #<Animal:0x00007fd4ddd2d980 @species="猩猩", @examples="小龐">
#=> default value with assigning arguments
class Animal
  def initialize(species = '猩猩', examples = '小龐')
    @species, @examples = species, examples
  end
end

orangutan = Animal.new #=> #<Animal:0x00007fd4ddd56d58 @species="猩猩", @examples="小龐">

另外我們也說明,括號在Ruby裡面可有可無,舉例來說,下列兩種宣告新物件的方法相同,不過依照慣例,若沒有必要使用初始值,我們不用寫括號。

orangutan = Animal.new
orangutan = Animal.new()

⭐️ 在一般的程式語言中,預設參數的給法若在順序上有差異,使用上也會有差別。

class Animal
  def initialize(species, examples = '小龐')
    @species, @examples = species, examples
  end
end

turtle = Animal.new('甲魚')  #=> #<Animal:0x00007fd4ddd763b0 @species="甲魚", @examples="小龐">

⭐️ 不過Ruby在某些狀況下會自己判斷預設值

class Animal
  def initialize(species = '猩猩', examples)
    @species, @examples = species, examples
  end
end

orangutan = Animal.new('阿豪') #=> #<Animal:0x00007fd4dddb6550 @species="猩猩", @examples="阿豪">

⭐️ 宣告新物件時,括號可以省略

orangutan = Animal.new('阿豪')
orangutan = Animal.new '阿豪'

⭐️ 有些特殊物件的宣告是可以不用透過new,像陣列、字串等。我們會比較偏愛literals constructor的用法勝於使用一般建構子的宣告方式

# 這樣用會比較冗長,比較少人會這樣用
ary = Array.new
str = String.new

# literal constructor
ary = [1, 2, 3]
str = "qwer"

⭐️ 看一下如何使用 hash 做為參數宣告

class Animal
  def initialize(species = '猩猩', examples:)
    @species, @examples = species, examples
  end
end

orangutan = Animal.new(examples: '阿豪') #=> #<Animal:0x00007fd4de0530d8 @species="猩猩", @examples="阿豪">

⭐️ Hash 搭配預設值

class Animal
  def initialize(species = '猩猩', examples: '小龐')
    @species, @examples = species, examples
  end
end

orangutan = Animal.new

⭐️ 使用 options = {} 搭配建構子宣告

class Animal
  def initialize(species = '猩猩', options = {})
    @species, @examples = species, options[:examples] || '小龐'
  end
end

orangutan = Animal.new  #=> #<Animal:0x00007fd4d3d1b8c0 @species="猩猩", @examples="小龐">

⭐️ 以下寫法會造成語法錯誤

# 語法錯誤  Predefined Args cannot allowed after named arg
class Animal
  def initialize(species = '猩猩', examples: '小龐', options = {})
    @species, @examples = species, examples
  end
end

⭐️ 預設值搭配Hash 的使用方法要注意

class Animal
  def initialize(species = '猩猩', options = {})
    @species, @examples = species, options[:examples] || '小龐'
  end
end

#=> {:a=>"a"} 被當作 @species
#=> #<Animal:0x00007fd4d794d028 @species={:a=>"a"}, @examples="小龐">
orangutan = Animal.new(a: 'a') 

# 預期內的行為
#=> #<Animal:0x00007fd4d9c8eb18 @species="猩猩", @examples="小龐">
orangutan = Animal.new('猩猩', a: 'a') 

hash預設值填在options裡面,而若宣告新物件給a: 'a',原本的examples: '小龐' 將會被覆蓋。

class Animal
  def initialize(species = '猩猩', options = {examples: '小龐'})
    @species, @examples = species, options[:examples]
  end
end

#=> #<Animal:0x00007fd4d9f67e70 @species="猩猩", @examples=nil>
orangutan = Animal.new('猩猩', a: 'a') 

我們也可以使用splat operator

class Animal
  def initialize(species = '猩猩', **options)
    @species, @examples = species, options[:examples] || '小龐'
  end
end

#=> #<Animal:0x00007fd4dd1195b8 @species="猩猩", @examples="小龐">
orangutan = Animal.new('猩猩', a: 'a')

block 可以當作變數使用

class Animal
  def initialize(species = '猩猩', it_day11)
    @species, @block = species, it_day11
  end
end

orangutan = Animal.new('猩猩', -> (x) {x + 1})
#=> #<Animal:0x00007fd4db77f558 @species="猩猩", @block=#<Proc:0x00007fd4db77f5a8@(irb):164 (lambda)>>

getter

Ruby的世界中,我們可以使用getter, setter 取得與改寫值。當我們想要取得動物的種類找不到,是因為我們沒有給方法取得值。

class Animal
  def initialize(species = '猩猩', options = {})
    @species, @examples = species, options[:examples] || '小龐'
  end
end

orangutan = Animal.new
orangutan.species  # NoMethodError (undefined method `species' for #<Animal:0x00007fd4dcad19a8>)

⭐️ 我們可以透過一些方法取得值

  • << 表示法 ➡️ Day12介紹
  • instance_eval ➡️ Day13介紹
# 法1. 透過 instance_eval 取得值
orangutan.instance_eval { @species }   #=> 猩猩
orangutan.instance_eval { species }    #=> 猩猩

# 法2. 對 orangutan 加入 getter 的單體方法,使其可以取用值
class << orangutan
  def species
    @species
  end
end

# 可以使用 getter
orangutan.species #=> 猩猩

⭐️ 接著開始介紹getter,以下為 getter的使用方式

class Animal
  def initialize(species = '猩猩', options = {})
    @species, @examples = species, options[:examples] || '小龐'
  end
  
  def species
    @species
  end
end

orangutan = Animal.new
orangutan.species        #=> 猩猩

⭐️Rubygetter ,有更簡潔的寫法

class Animal
  def initialize(species = '猩猩', options = {})
    @species, @examples = species, options[:examples] || '小龐'
  end
  
  attr_reader :species
end

orangutan = Animal.new
orangutan.species         #=> 猩猩

setter

⭐️ 我們可以利用setter 來去設定實體變數

class Animal
  def initialize(species = '猩猩', options = {})
    @species, @examples = species, options[:examples] || '小龐'
  end
  
  # getter
  attr_reader :species
  
  # setter
  def species=(species)
    @species = species
  end
end

orangutan = Animal.new
orangutan.species = '猩'
orangutan.species          #=> 猩

⭐️Ruby也有更簡潔的用法

class Animal
  def initialize(species = '猩猩', options = {})
    @species, @examples = species, options[:examples] || '小龐'
  end
  
  # getter
  attr_reader :species
  
  # setter
  attr_writer :species
end

orangutan = Animal.new
orangutan.species = '猩'
orangutan.species          #=> 猩

attr_accessor

⭐️ 對於取值、改寫值的用法,Ruby 提供了 attr_accessor 作為getter, setter 使用。

class Animal
  def initialize(species = '猩猩', options = {})
    @species, @examples = species, options[:examples] || '小龐'
  end
  
  # getter/setter
  attr_accessor :species
end

orangutan = Animal.new
orangutan.species = '猩'
orangutan.species          #=> 猩

⭐️ 若要對getter, setter 進行客製化功能的話,只能夠自己寫

class Animal
  def initialize(species = '猩猩', options = {})
    @species, @examples = species, options[:examples] || '小龐'
  end
  
  attr_reader :species
  
  # setter
  def species=(species)
    @species = "❌ #{species}"
  end
end

orangutan = Animal.new
orangutan.species = '猩'
orangutan.species          #=> "❌ 猩"

instance method

下列的例子中,drive為物件的方法,稱為instance method

class Car
  def initialize(color, size)
    @color, @size = color, size
  end
  
  def drive
    [color.capitalize, 'car', 'boo boo!'].join(' ')
  end
  
  private
  
  attr_reader :color
end

red_car = Car.new('red', 'big') 
blue_car = Car.new('blue', 'big')  

red_car.drive    #=> "Red car boo boo!"
blue_car.drive   #=> "Blue car boo boo!"

首先要澄清兩件事情:

  • 若不寫attr_reader 一樣可以動作。

  • colorself.color的簡寫,這邊我們改使用實體變數@color也可以。個人習慣在唯讀的變數加入attr_reader,並且毋需在外部取得值的地方,例如上例的@color,漢漢老師就會把它寫在private裡面,避免外部可以取得值。

Ruby的世界中,一共有public, private, protected三種方法,目前還沒有用過protected過,還沒有碰過需要區分的情境。若好奇的參考 這篇 論述即可。

class Car

  # public 方法
  
  private
  
  # 私有方法
  
  protected
  
  # 保護方法
end

analyse_color可以得知,self.變數等於實體變數。此外,object_id可以用來判斷物件實際的位址。

class Car
  def initialize(color, size)
    @color, @size = color, size
  end
  
  def reflect
    self
  end
  
  def analyse_color
    color.object_id == @color.object_id
  end
end

red_car.reflect             #=> #<Car:0x00007fd065a07520 @color="red", @size="big">
red_car.reflect == red_car  #=> true

red_car.analyse_color

instance variable

實體變數為長相@開頭的變數,都為實體變數instance variable。上面已經提到了很多實體變數的觀念,這邊會再詳述實體變數的概念。

@color, @size, @species, @examples

實體變數是內部存放資料的地方,我們可以利用實體變數來取值和給值,並且我們不一定要在初始化的時候宣告實體變數。下例即為令新的實體變數@age,並給他get, set兩種方法(複習一下attr_accessor

class Car
  def initialize(color = 'red', size = 'big')
    @color, @size = color, size
  end
  
  attr_accessor :age
end

car = Car.new
car.age = 20
car.age      #=> 20

我們實際看 car 物件的實體變數

car.instance_variables
#=> [:@color, :@size, :@age]

結尾語

這邊花了很大的篇幅,介紹了class的基本用法,後面的章節會介紹類別方法。

如今已經到了第11天,看著訂閱數與觀看數的微幅增長,確實比起跟著股票的漲跌而心情起舞,感受還要好。看到有人願意花時間點開我的文章,我真的很開心。

第一天列出了這一期鐵人賽的大綱,在寫的過程中一直改變想法,30篇的文章架構不斷的再改變。一來是下班時間並沒有那麼多,加上若一篇文章訊息量太大,反而會造成反效果。所以這陣子不斷的調整文章,調整能夠讓讀者看到更精闢的重點!

我也推薦同樣是工程師的朋友們寫文章,雖然表面上好像很花時間,比起自己在邊工作的時候,只記自己看得懂的筆記。多用心記錄,整理成主題式的文章,未來再反芻自己的文章也比較不會看不懂,其實算是很好的投資。

參考資料


上一篇
Day10. 深入瞭解 Block - Block Part3
下一篇
Day12. Class Method 與 MetaClass 的觀念
系列文
初階 Rails 工程師的養成34
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言