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,等同类别方法。

参考资料


<<:  D7 allauth 采坑日记 Extending & Substituting User model (2)

>>:  Day 0xC UVa10170 The Hotel with Infinite Rooms

想知道目标客群喜欢什麽?何不从搜寻意图下手!

面对面实体销售,我们能从聊天问答、肢体行为初步了解客户需求,但──在网路世界,如果你想知道客户喜欢什...

Day. 27 Binary Tree Level Order Traversal

Leetcode #102. Binary Tree Level Order Traversal 简...

Day 26 Wireless Attacks - 无线攻击 (aircrack-ng)

前言 终於进入新的篇章06-Wireless Attacks,但由於先前的Kali虚拟机环境无法进行...

Day11 iPhone捷径-媒体Part1

Hello 大家, 明明是普通的周末, 不知为何这周一堆人出去玩@@ 我错过了甚麽吗? 今天来讲媒体...

[Day1] 关於资料库30天的发文方向

这一个月的不间断发文,会将大二下学期必修的资料库概论浓缩成30篇文,并将当中所学,纪录的课程笔记再整...