iT邦幫忙

2021 iThome 鐵人賽

DAY 13
0
Modern Web

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

Day13. class_eval & instance_eval - 解答什麼是 MetaClass & Singleton

接下來介紹的章節,會使用到instance_eval, class_eval,加上我們已經在 Day12 提到的MetaClassSingleton的概念。因此在使用前,今天會先介紹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_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_evalApple新增了名叫color的方法,此方法與實體方法無異。

instance_eval & singleton

首先,我們可以對一個實體,透過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_stringnew_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 with instance_variable & private method

此外,我們也可以透過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種可以使用的情境。

  • Rake
  • as_json
  • 輸出表單
  • 搭配權限管理
  • #included 繼承鍊

Rake

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

as_json

作為 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)

https://ithelp.ithome.com.tw/upload/images/20210912/201158542XLGrPfSX3.png

我們可以用以下 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

Eigenclass & Singletion pattern

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

    如果是對 classinstance_eval,等同類別方法。

參考資料


上一篇
Day12. Class Method 與 MetaClass 的觀念
下一篇
Day14. Module & #extend #prepend #include - Ruby 繼承 part1
系列文
初階 Rails 工程師的養成34

尚未有邦友留言

立即登入留言