接下來介紹的章節,會使用到instance_eval
, class_eval
,加上我們已經在 Day12 提到的MetaClass
和 Singleton
的概念。因此在使用前,今天會先介紹instance_eval
, class_eval
的使用方式,以及相對應的使用情境。
先講對同一 class
使用 instance_eval
, class_eval
所產生的結果
class_eval
➡️ 新增實體方法instance_eval
➡️ 新增類別方法class A
end
# 新增實體方法
A.class_eval do
def foo
'foo'
end
end
# 新增類別方法
A.instance_eval do
def bar
'bar'
end
end
A.bar #=> 'bar'
A.new.foo #=> 'foo'
class_eval
的概念很簡單,即為在class
新增方法,而class
內部的方法為實體方法。我們舉個例子:
#======= 正規寫法
class Apple
def color
'red'
end
end
apple = Apple.new
apple.color #=> 'red'
#======= 使用class_eval: 對 class 新增方法
class Apple
end
Apple.class_eval do
def color
'red'
end
end
apple = Apple.new
apple.color #=> 'red'
使用class_eval
為Apple
新增了名叫color
的方法,此方法與實體方法無異。
首先,我們可以對一個實體,透過instance_eval
定義新方法。
my_string = "String"
my_string.instance_eval do
def new_method
self.reverse
end
end
my_string.new_method => "gnirtS"
我們對my_string
新增方法,就如同 Day12 提到的MetaClass
的觀念一樣。我們為my_string
新增了單體方法singleton
,所有用字串String
創建的物件裡面,只有my_string
有new_method
這個獨特技能!
order = Order.first
order.instance_eval do
def foo
'bar'
end
def info
{id: id, price: price}
end
end
order.foo #=> "bar"
order.info #=> {:id=>1, :price=>18100}
如上所示,我們可以為特地的訂單新增方法!不過照上面的邏輯,如果有100張訂單,我們就要創建100個foo
, info
共200個方法。不如在每個order
的原型 Order
,所以以上的情況,我們會使用 class_eval
直接在Order
定義實體方法。
對單體新增方法,只有在比較特殊的情境會用到!目前漢漢老師還沒有碰過這種需求
之前我們提到,在Ruby的程式語言中,任何東西都是物件,包括使用Apple
新增的apple
物件,以及Apple
本身也是物件。
# 物件
apple = Apple.new
# 也是物件
Apple
既然是物件,我們可以對apple
使用instance_eval
,沒有道理不能對Apple
也使用instance_eval
。
Day12 我們提到將所有的猩猩加護,所有的猩猩都能夠使用bar
技能,而bar
為類別方法
class Orangutan
end
def Orangutan.bar
'bar'
end
Orangutan.bar #=> bar
同樣,我們可以透過instance_eval
新增類別方法bar
,替所有的猩猩都增加同樣的技能。
class Orangutan
end
Orangutan.instance_eval do
def bar
'bar'
end
end
Orangutan.bar #=> bar
換個角度來思考,我們使用instance_eval
,或使用 <<
新增方法,都是為物件新增一個單例方法,只不過當我們使用在apple
, orangutan
,成為了apple
, orangutan
的獨特行為,而若使用在Apple
, Orangutan
則為類別方法。
我們使用singleton_methods
可以找到 Orangutan
的單體方法 bar
Orangutan.singleton_methods #=> [:bar]
此外,我們也可以透過instance_eval
取得實體變數 & 和私有方法
class Apple
def initialize(color = 'red')
@color = color
end
private
attr_accessor :color
def private_foo
'foo'
end
end
apple = Apple.new
#=> #<Apple:0x00007fd8888b9d28 @color="red">
apple.color
# NoMethodError (private method `color' called for #<Apple:0x00007fd8848203e8 @color="red">)
apple.private_foo
# NoMethodError (private method `private_foo' called for #<Apple:0x00007fd8848203e8 @color="red">)
# 取得實體變數 & 和私有方法
apple.instance_eval {color} #=> "red"
apple.instance_eval {private_foo} #=> "foo"
最重要的,還是如何在實際應用中使用class_eval
, instance_eval
,以下舉例5種可以使用的情境。
#included
繼承鍊namespace :data do
desc '同步資料'
task sync_data: :environment do
Order.class_eval do
scope :bar, -> { 'bar' }
scope :reviewed, -> { ... }
scope :yet_not_sync, -> { ... }
scope :week_ago, -> { where('done_at < ?', Rails.env.production? ? 1.day.ago : 1.minute.ago) }
# class_methods
class << self
define_method :foo, -> { 'foo' }
end
end
Order.yet_not_sync.week_ago.reviewed.each do |order|
begin
...
rescue
p "同步失敗"
end
end
end
end
作為 react_component
的傳遞資料使用,我們可以使用 class_eval
替原本的model
產生更多方法
# app/helpers/member_helper.rb
module MemberHelper
# react component with props data
def member_helper
react_component("MemberOrder", props: react_store_member_params)
end
# params props in react
def react_store_member_params
Order.class_eval do
define_method :foo, -> { 'foo' }
def bar
'bar'
end
end
{ orders: @orders.order('created_at desc').includes(:product).as_json(methods: [:foo, :bar]) }
end
end
在畫面中呼叫member_helper
,即可使用 react
元件。
= member_helper
輸出表單前,我們需要整理資料。整理多量model資料的工作可以交給class_eval
去做
# orders_helper.rb
module Admin::OrdersHelper
extend ActiveSupport::Concern
# 寫進Excel表單的內容
def write_sheet(wb, sheet_name, orders)
wb.add_worksheet(name: sheet_name) do |sheet|
...
# 訂單資料
orders.each do |order|
...
order.order_items.each_with_index do |item, index|
_data = []
_data << order.export_shipping_type
_data << item.export_sku
_data << item.export_preorder
...
end
end
end
end
included do
Order.class_eval do
def export_shipping_type
shipping_type.present? ? I18n.t("order.shipping_type.#{shipping_type}") : ''
end
end
OrderItem.class_eval do
def export_sku
variant.sku
end
def export_preorder
is_preorder ? 'v' : ''
end
end
end
end
下列的程式碼中,can_view?(:user)
是Rails helper
定義的方法。在 Day9 提到過,do end
包起來的部分為獨立的領域,外面的變數無法和裡面交流,因此在 ReturnOrder, SubOrder 的 instance_eval
(or class_eval
) 的 block
內部看不懂can_view?(:user)
,因此造成錯誤
# 錯誤使用方法: can_view?(:user)
module Admin::SidebarHelper
[SubOrder, ReturnOrder].each do |name_model|
name_model.instance_eval do
def with_permission
if can_view?(:user)
user_brand_ids = User.first.brands.pluck(:id)
name_model.where(brand_id: user_brand_ids)
else
name_model.where(store_id: current_user.store_id)
end
end
end
end
end
圖解領域所造成的影響,會造成 instance_eval 包覆內的區塊看不懂 can_view?(:user)
我們可以用以下 2 種寫法改寫
# 方案1: 不用 instance_eval
module Admin::SidebarHelper
def with_permission(name_model)
if can_view?(:user)
user_brand_ids = User.first.brands.pluck(:id)
name_model.where(brand_id: user_brand_ids)
else
name_model.where(store_id: current_user.store_id)
end
end
end
With_permission(ReturnOrder)
# 方案2: 傳參數進去
module Admin::SidebarHelper
[SubOrder, ReturnOrder].each do |name_model|
name_model.instance_eval do
def with_permission(permission)
if permission
user_brand_ids = User.first.brands.pluck(:id)
name_model.where(brand_id: user_brand_ids)
else
name_model.where(store_id: current_user.store_id)
end
end
end
end
end
ReturnOrder.with_permission(can_view?(:user))
在Day14, Day15我們會講到的繼承所搭配的4個hook
,適合搭配instance_eval
, class_eval
module#included
,module#prepended
,module#extended
,class#inherited
module Notification::Helper
def self.included(base)
base.class_eval do
# ...
end
base.instance_eval do
# ...
end
end
end
Day12和今天所介紹的內容為OOP的其中一種設計流程 ➡️ Singleton pattern
class Apple
attr_accessor :color
def initialize(color = 'red')
@color = color
end
def description
[color, 'Apple'].join(' ')
end
end
apple = Apple.new
another = Apple.new
# 稱為 Eigenclass, Singleton class
def another.description
'金蘋果'
end
apple.description #=> "red Apple"
another.description #=> '金蘋果'
apple.singleton_methods #=> []
another.singleton_methods #=> [:description]
類別方法也是一種單體方法
class Apple
def self.ad
'大家來買蘋果'
end
end
Apple.singleton_methods.include?(:ad) #=> true
Apple.instance_methods.include?(:ad) #=> false
我們為class_eval
, instance_eval
來做總結
class_eval
作用於 class,並可以用來定義 instance_methods
instance_eval
作用於 instance,並可以用來定義 singleton_methods
如果是對 class
做instance_eval
,等同類別方法。