ruby 元编程用例
method_missing
很神奇,但也不少坑
打开类
与其死记硬背 strftime
的格式化参数,或者每次都复制粘贴
Time.now.strftime("%Y-%m-%d %H:%M:%S")
# => "2019-09-10 16:37:11"
不如打开 Time
这个类,加些语法糖进去
### lib/time.rb
[:to_js_timestamp, :to_s_datetime, :to_s_date].each do |m|
# 避免复写已有方法
#if Time.instance_methods(:false).grep(m).empty?
if !Time.instance_methods(:false).include?(m)
class Time
# 时间转 javascript 时间戳
def to_js_timestamp
(self.to_f * 1000.0).to_i
end
# 时间格式化成"年-月-日 时-分-秒"
def to_s_datetime
strftime("%Y-%m-%d %H:%M:%S")
end
# 时间格式化成"年-月-日"字符串
def to_s_date
strftime("%Y-%m-%d")
end
end
end
end
#if Time.methods.grep(/from_js_timestamp/).empty?
if !Time.methods.include?(:from_js_timestamp)
class Time
# javascript 时间戳转时间
def self.from_js_timestamp(ms)
Time.at((ms/1000).to_i)
end
end
end
pry
[1] pry(main)> Time.methods.include?(:from_js_timestamp)
=> false
[2] pry(main)> load 'lib/time.rb'
=> true
[3] pry(main)> Time.methods.include?(:from_js_timestamp)
=> true
[4] pry(main)> Time.from_js_timestamp(Time.now.to_i * 1000)
=> 2019-10-30 10:42:57 +0800
[5] pry(main)> [:to_js_timestamp, :to_s_datetime, :to_s_date].each do |m|
[5] pry(main)* p Time.now.public_send(m)
[5] pry(main)* end
1572405151510
"2019-10-30 11:12:31"
"2019-10-30"
祖先链:父类继承和模块包含
父类继承:Rails 的 ApplicationController
class ApplicationController < ActionController::Base
def current_user
@current_user = User.find_by(id: params[:id])
end
def check_login!
return render status: 403 if !current_user
end
end
class SomeController < ApplicationController
before_action :check_login!, except: [:actions_do_not_require_login]
def most_actions
end
def actions_do_not_require_login
end
end
模块包含:Rails 的 Concern
# app/models/concerns/uuid_helper.rb
require 'snowflake-rb'
module UuidHelper
def self.included(base)
base.before_create :gen_outid
end
private
def gen_outid
sf = Snowflake::Rb.snowflake(0, 0)
self.out_id = sf.next.to_s if out_id.blank?
end
end
# User 表已有 out_id 字段
class User < ApplicationRecord
include UuidHelper
end
# Order 表也已有 out_id 字段
class Order < ApplicationRecord
include UuidHelper
end
动态方法
(任意)方法调用:(send)public_send
# 根据时间段(day/ week/ month)
# 动态调用 Rails 的 beginning_of_xxx 和 end_of_xxx
# 返回时间段开始、结束时间字符串
# 例如当日 2019092520190925
module RangeHelper
def gen_range_no(range)
begined_at = Time.now.public_send("beginning_of_#{range}")
ended_at = Time.now.public_send("end_of_#{range}")
begined_at.strftime("%Y%m%d") + ended_at.strftime("%Y%m%d")
end
end
class Ranking
include RangeHelper
# 根据时间段(day/ week/ month)
# 返回唯一字符串,用作排行榜缓存的键值
# 例如日榜 ranking/day/2019092520190925
def range_lb_name(range)
date_str = gen_range_no(range)
"ranking/#{range}/#{date_str}"
end
end
运行时定义:define_method
# 根据日期和时间段(day/ week/ month)
# 批量生成排行榜名称,用作排行榜缓存的键值
# 例如日榜 day_lb_name 返回 ranking/day/2019092520190925
class Ranking
include RangeHelper
['day', 'week', 'month'].each do |range|
define_method "#{range}_lb_name" do
date_str = gen_range_no(range)
"ranking/#{range}/#{date_str}"
end
end
end
Ranking.instance_methods.grep(/_lb_name$/)
#=> [:day_lb_name, :range_lb_name, :week_lb_name, :month_lb_name]
幽灵方法:method_missing
下面的例子其实不是很好。method_missing
比较适用的场景应该是 active-record
或者 builder
这种针对大量未知属性,自动生成读写方法的库,目的是为了自适应。而可能性已知或有限的情况下,只是为了消除重复代码,用 define_method
更合适。鉴于 method_missing
可能会带来副作用,记住这条原则:use Dynamic Methods if you can and Ghost Methods if you have to.
# 消除 model 中相似的 scope
def self.method_missing(name, day)
m = name.to_s.gsub('by_', '')
super if !day.respond_to?("beginning_of_#{m}") || !day.respond_to?("end_of_#{m}")
where(paid_at: day.send("beginning_of_#{m}")..day.send("end_of_#{m}"))
end
# 用 define_method 更合适
class << self
['day', 'week', 'month'].each do |name|
define_method "by_#{name}" do |day|
where(paid_at: day.send("beginning_of_#{name}")..day.send("end_of_#{name}"))
end
end
end
与改造前的 scope 功能一致
[1] pry(main)> PromoOrder.by_week(Time.now)
PromoOrder Load (22.0ms) SELECT "promo_orders".* FROM "promo_orders" WHERE ("promo_orders"."paid_at" BETWEEN $1 AND $2) [["paid_at", "2019-11-03 16:00:00"], ["paid_at", "2019-11-10 15:59:59.999999"]]
=> []
确认不会带来副作用。没有定义的方法依然返回 NoMethodError
[2] pry(main)> PromoOrder.by_weak(Time.now)
NoMethodError: undefined method `by_weak' for #<Class:0x00007ff827ea83d8>
from /Users/justin/.rvm/gems/ruby-2.6.3/gems/activerecord-5.1.3/lib/active_record/dynamic_matchers.rb:22:in `method_missing'
Reference
Programming Ruby - Metaprogramming Ruby 2. Program Like the Ruby Pros (2014)