模擬class物件:Ruby當中Struct及OpenStruct的使用

Adler @ 2015-04-06


為什麼我們需要模擬class物件呢?主要是一個物件有一些屬性需要存取,例如一篇文章Post底下需要titlecontent兩個屬性,用class來存取就是用牛刀殺雞,太過複雜,用簡單的Hash存取即可。

Hash其實在某些功能上過於簡單,存取的功能較不方便,假如要模擬的class更為複雜,就需要OpenStruct協助。

OpenStruct

OpenStructHash一樣可以自帶屬性:

require 'ostruct'
# OpenStruct class不包含在原本Core物件當中,因此需要先require

book = OpenStruct.new(title: "Harry Potter", episodes: 7)

# 讀取
book.title # => "Harry Potter"
book[:title] # => "Harry Potter"
book["title"] # => "Harry Potter"

# 存入
book.title = "Holmes" # => "Holmes"
book[:title] = "Holmes" # => "Holmes"
book["title"] = "Holmes" # => "Holmes"

# 隨意新增屬性
book.content = "book content"
book.content
# => "book content"

以上可以看出,OpenStruct最方便的地方就是可以把屬性當做method來處理,可以直接指定屬性內容、直接讀取,且不管使用string或symbol當做key都通用。

如果要把一整個Hash的數值import到裡面也很簡單:

hash = Hash(:popularity => "well-known", :author => "J.K. Rowling")

hash.each do |key, value|
  book[key] = value
end

OpenStruct的自由程度比較接近Hash,可以隨時新增及定義屬性,但沒辦法定義method。如果要加入method,則必須使用Struct

Struct

Struct稍微複雜一些,比較接近於原本class的使用。

# 宣告時需要先行定義屬性
Book = Struct.new(:title, :episodes)
book = Book.new("Harry Potter", 7)

book
# => #<struct Book title="Harry Potter", episodes=7>

# 讀取的方式與OpenStruct一樣自由
book.title
# => "Harry Potter"
book[:title]
# => "Harry Potter"
book["title"]
# => "Harry Potter"

# 但無法隨時新增數值
book.content = "book content"
# => NameError: no member 'content' in struct

# 若在宣告Struct instance時,未帶入的變數會自動變成nil
book = Book.new("Harry Potter")
book.episodes
# => nil

# 同樣的,宣告時帶入太多參數會產生錯誤
book = Book.new("Harry Potter", 7, "other stuff")
# => ArgumentError: struct size differs

Struct很威的地方是可以在定義時帶入block,並定義method:

Book = Struct.new(:title, :episodes) do
  def excerpt
    "The book title is #{title}, which contains #{episodes} episodes."
  end
end

book = Book.new("Harry Potter", 7)
book.excerpt
# => "The book title is Harry Potter, which contains 7 episodes."

實際應用

我自己最常用於寫Rails測試的時候使用Struct,例如撰寫RSpec的unit test要將其他class物件使用mock區隔開來。

原本的code

class Post < ActiveRecord ::Base
  def duplicate
    Tool.new.duplicate
  end
end

一般撰寫unit test,為求獨立測試,所以會將其他class物件隔離,所以我們就用Struct將上方的Tool這個class給mock掉。

測試code

require 'spec_helper'
describe Post do
  it "#duplicate" do
    stub_const("Tool", Struct.new(){
      def duplicate
        "It is duplicated!"
      end
    })
    expect(subject.duplicate).to be_truthy
  end
end

執行時,由於Tool這個class已經被stub_const,所以就不會去讀取原本的class內容,而是讀取我在測試中設定的Struct,並回傳"It is duplicated!"字串。成功的執行了這個unit test!

效能

在效能比較上,StructOpenStruct有非常大的差異。如StackOverflow上這篇比較所述,可以看出在非常簡單的物件存取上,大概會有5~6倍的效能差異。如果在Application內實際應用大量的OpenStruct,可能會有效能的問題,建議優先refactory為Struct物件。

延伸閱讀:Logical Bricks 圖片來源:WikiMedia