Ruby 学习笔记簿:Metaprogramming Workshop - Before Action

实作前准备

需要先了解以下主题:

此实作范例修改自五倍红宝石技术文章

范例题目

目标是让下面的程序码:

1     class Example
2       before_action :method_a, :method_b do
3         |n| puts "the code before #{n}"
4       end
5
6       def method_a
7         puts 'this is method a'
8       end
9
10      def method_b
11        puts 'this is method b'
12      end
13    end
14
15    instance_1 = Example.new
16    instance_1.method_a
17    instance_2 = Example.new
18    instance_2.method_b
19

可以有这样的执行结果

1      the code before method_a
2      this is method a
3      the code before method_b
4      this is method b

实作的过程

步骤ㄧ:建立 before_action 类别方法

首先是打开 Singleton Class 的起手式:

class 类别名称
  class << self
    def 方法名称
    end
  end
end
class Example
  class << self
  attr_accessor :methods, :block

    def before_action

    end
  end
end

这里加入了 before_action 的类别方法,之後覆写方法时,会需要取得到相关的变数,所以也ㄧ并把 attr_acessor 所提供方法都带进去了。这代表了我们ㄧ共建立了五个类别方法:(1) :before_action, (2) :methods, (3) :methods=, (4) :block, (5) :block=

步骤二:从引数读取执行的 (1) 方法名称 (2) Block 内容

在定义 before_action() 方法会需要两个参数(parameter):

  1. 方法名称(*methods):由於传进来名称的数量不确定,所以用星号 splat operator(*)。
  2. 执行内容(&block):依附在 before_action() 的程序码区块 , 注意这里的 & 符号会将 block 转成 Proc 物件。

因为在执行 before_action() 方法时,还未定义 Example 类别内的 method_a() 方法,无法在这里执行覆写的编码,但可以把传进来的引数储存下来,而透过 attr_accessor 就可以让其他的方法取得存下来的属性。

class Example
  class << self
    attr_accessor :methods, :block
    def before_action(*methods, &block)
      @methods = methods
      @block = block
    end
  end
end

步骤三:覆写每一个要执行的方法

接着我们建立 ExampleRefinement 模组,以 refine 的方式改写 Example 类别内要被覆写的方法。这时候之前储存下的的属性就派上用场了

透过 each 方法把每一个方法名称转出来,以 alias_method 建立方法的新别名,并且保留原有方法的功能。接着用动态方法 define_method 重新定义方法,要执行的内容会先呼叫 block ,再用 send 呼叫原本的 method。

在最後的步骤中,透过运用动态方法、method wrappers的技巧来达到覆写方法的目的。

module ExampleRefinement
  refine Example do
    block = Example.block
    @methods = Example.methods

    @methods.each do |method|
      newname = "new_#{method}"
      alias_method newname, method

      define_method method do
        block.call(method)
        send(newname)
      end
    end
  end
end

完整程序码

class Example
  class << self
    attr_accessor :methods, :block
    def before_action(*methods, &block)
      @methods = methods
      @block = block
    end
  end
end

class Example
  before_action(:method_a, :method_b){|n| p "the code before #{n}"}

  def method_a
    p 'this is method a'
  end

  def method_b
    p 'this is method b'
  end
end

module ExampleRefinement
  refine Example do
    block = Example.block
    @methods = Example.methods

    @methods.each do |method|
      # 帮原有的方法取别名
      newname = "new_#{method}"
      alias_method newname, method

      # 动态定义新方法要执行的内容
      define_method method do
        block.call(method)
        send(newname)
      end
    end
  end
end

using ExampleRefinement     # => 拿掉这一行就不会有覆写方法的效果
instance_1 = Example.new
instance_1.method_a
instance_2 = Example.new
instance_2.method_b

执行结果会是:

the code before method_a
this is method a
the code before method_b
this is method b

五倍范例题目

目标是让下面的程序码:

1      class Example
2        extend BeforeAction
3
4        before :method_a, :method_b do
5          puts "the code before method"
6        end
7
8        def method_a
9          puts 'this is method a'
10       end
11
12       def method_b
13         puts 'this is method b'
14       end
15     end
16
17     instance_1 = Example.new
18     instance_1.method_a
19     instance_2 = Example.new
29     instance_2.method_b

可以有这样的执行结果

1      the code before method
2      this is method a
3      the code before method
4      this is method b

实作的过程在原文章的最後部分,详细的解说可以点选 这里

五倍完整程序码

以下程序码(加上我自己的注解)是完整的答案:

module BeforeAction
  def new
    # 只有第一次执行 new 时要 execute_before
    # 因为 execute_before 内的 alias_method(),在执行第二次时会有
    # 无限回圈的问题
    execute_before if first_time?
    
    # 再呼叫 super 才能回到原有的 new 方法
    super
  end

  def first_time?
    return false if @not_first_time
    @not_first_time = true
  end

  def before(*methods, &block)
    # 先存起来之後用
    @methods = methods
    @block = block
  end

  def execute_before
    @methods.each do |method|

      # 帮原本的 method 取一个新名字
      newname = "new_#{method}"
      alias_method newname, method
      block = @block

      # 重新定义每个 method 要做什麽
      define_method method do
        # 执行 block
        block.call
        
        # 原本的 method
        send(newname)
      end
    end
  end
end

class Example
  extend BeforeAction
  before :method_a, :method_b do
    puts 'the code before method'
  end

  def method_a
    puts 'this is method a'
  end

  def method_b
    puts 'this is method b'
  end
end

instance_1 = Example.new
instance_1.method_a
instance_2 = Example.new
instance_2.method_b

Medium 文章连结:https://kevin0117.medium.com/

本文同步发布於 Kevin's Blog: https://chienhao.tw/

备注:之後文章修改更新,以个人部落格为主


<<:  Amazon SageMaker 机器学习线上研讨会

>>:  38.vue.config.js

EC2上的资料库

除了AWS提供预设的RDS, 若觉得使用起来不顺手, 也可以建立虚拟机, 使用安装版的MSSQL资料...

30-27 之 DDD 战略设计 3 - 实作方法之 Domain Storytelling 领域叙事 ( 未完成 )

上一篇我们简单的说明完如何使用 Event Storm 来完成 DDD 战略的三个产出 : 分析 D...

Microsoft Windows VirtualDesktop 系列纪录 - MSIX AppAttach

MSIX AppAttach 其实让我想到了十年前刚接触Citrix XenApp跟VMware V...

Day4 中秋节就是要烤肉阿-韩式烤五花肉

中秋节就是要烤肉阿! 台式烤肉吃腻了来换换口味吧, 韩剧及韩综中常常出现韩国烤五花肉,在家就可以吃!...

Day03 UIKit 02 - App Delegate

AppDelegate 为App 的主要入口点,Apple 会在一些应用程序级别的生命周期事件调用A...