alias_method_chain
概要
この記事は、「メタプログラミングRuby 第2版」第11章「alias_method_chainの盛衰」を読み学習した内容を個人学習用にまとめ直したものです。
Railsにはかつてalias_method_chainというメソッドが存在し、既存メソッドの前後に処理を挟む「アラウンドエイリアス」パターンを簡潔に実現していました。本記事では、メタプログラミングの観点から、alias_method_chainがどのような課題を解決したのか、そしてシンプルなオーバーライドやRuby 2.0で導入されたModule#prependによってどのように役割を終えていったのかを読み解きます。
alias_method_chain
冗長なエイリアス設定
以下のようなモジュールのメソッドに新機能を追加する場合、「メソッド」「メソッド_without_機能」「メソッド_with_機能」という命名規則に従った3つのメソッド名を用意し、エイリアスで旧名メソッドと「メソッド_without_機能」の参照先を切り替えるのがRailsにおいては一般的。
module Greetings
def greet
"hello"
end
end
class MyClass
include Greetings
def greet_with_enthusiasm
"Hey, #{greet_without_enthusiasm}!"
end
alias_method :greet_without_enthusiasm, :greet # 元の内容のメソッド処理を呼び出す場合のエイリアス
alias_method :greet, :greet_with_enthusiasm # 元の名前を呼び出した場合に、新しい機能を呼び出すエイリアス
end
MyClass.new.greet # => "Hey, hello!"
MyClass.new.greet_without_enthusiasm # => "hello"
Railsでは一般的な手法だが、この方法だと上記と同じような冗長な記述が機能を追加したいメソッドごとに複製されることとなる。
この問題を解決するために用意されたのが、alias_method_chainである。
alias_method_chainの中身
alias_method_chainの実装は、先ほどの冗長なエイリアス設定をメタプログラミングで自動化したものである。
class Module
# ターゲットとなるメソッドと機能追加メソッドの名前を引数として受け取る
def alias_method_chain(target, feature)
# メソッド名の末尾に`?`、`!`、`=`がある場合にも対応するため、`sub`で記号を分離している
aliased_target, punctuation = target.to_s.sub(/([?!=])$/, ""), $1
yield(aliased_target, punctuation) if block_given?
# target_with_feature, target_without_featureを定義する
with_method = "#{aliased_target}_with_#{feature}#{punctuation}"
without_method = "#{aliased_target}_without_#{feature}#{punctuation}"
# エイリアスの設定
alias_method without_method, target
alias_method target, with_method
# 元メソッドのアクセス修飾子を保持する
# without_methodは元メソッドのコピーなので、その可視性を調べて差し替え後のtargetに再適用する
case
# 元メソッドがpublic, protect, privateだったら同じ可視性に戻す
when public_method_defined?(without_method)
public target
when protected_method_defined?(without_method)
protected target
when private_method_defined?(without_method)
private target
end
end
end
alias_method_chainが実際にRailsでどう使われていたかの例として、ActiveRecord::Validationsの抜粋を示す。saveメソッドにバリデーション機能を追加するために使用されていた。
module ActiveRecord
module Validations
def self.included(base)
base.class_eval do
# saveにvalidation機能を追加
# saveを呼ぶとsave_with_validation、元のsaveはsave_without_validationで呼べるようになる
alias_method_chain :save, :validation
alias_method_chain :save!, :validation
end
end
# alias_method_chainの設定と同時に以下の二つのメソッドも定義する必要がある
def save_with_validation(perform_validation = true)
if perform_validation && valid? || !perform_validation
save_without_validation
else
false
end
end
def save_with_validation!
if valid?
save_without_validation!
else
raise RecordInvalid.new(self)
end
end
end
end
alias_method_chainが不要となる代替策1(シンプルなオーバーライド)
alias_method_chainは以下のような、シンプルなオブジェクト指向的な解決策で代用できる。
module Greetings
def greet
"hello"
end
end
class MyClass
include Greetings
end
MyClass.new.greet # => "hello"
greetメソッドに機能を追加する際にエイリアスを使うのではなく、別のモジュールにgreetメソッドを定義してオーバーライドし、そちらをインクルードすればalias_method_chainと同様の挙動がよりシンプルに実現できる。
module EnthusiasticGreetings
include Greetings # 元のGreetingsを継承チェーンに追加
def greet
"Hey, #{super}!" # 元の実装をsuperで呼び出して機能を追加
end
end
class MyClass
include EnthusiasticGreetings
end
MyClass.ancestors[0..2] # => [MyClass, EnthusiasticGreetings, Greetings]
MyClass.new.greet # => "Hey, hello!"
現代のRailsのActiveRecord::Validationsも、この方法で実装されている。
alias_method_chainによるエイリアスの差し替えではなく、同名のsaveメソッドを定義してsuperで元の実装に委譲するシンプルな構造になっている。
# 現代のRailsのActiveRecord::Validationsより抜粋・簡略化
module ActiveRecord
module Validations
# 妥当性確認が成功したら通常のsaveのコードを実行し、失敗したらfalseを戻す
def save(**options)
perform_validations(options) ? super : false
end
def save!(**options)
perform_validations(options) ? super : raise_validation_error
end
private
def perform_validations(options = {})
options[:validate] == false || valid?(options[:context])
end
end
end
alias_method_chainが不要となる代替策2(Module#prepend)
前項のシンプルなオーバーライドによるalias_method_chainの代替はModuleのメソッドにおいては有用だが、以下のようなクラスに直接定義されたメソッドにおいては、継承チェーンでクラスよりも祖先側にインクルードされてしまうのでオーバーライドすることができない。
module EnthusiasticGreetings
def greet
"Hey, #{super}!"
end
end
class MyClass
include EnthusiasticGreetings
def greet
"hello"
end
end
# MyClassの上にincludeされている
MyClass.ancestors[0..2] # => [MyClass, EnthusiasticGreetings, Object]
# EnthusiasticGreetings#greetをMyClass#greetが上書きしてしまう
MyClass.new.greet # => "hello"
この解決策として、prependを使用しEnthusiasticGreetingsをMyClassの継承チェーンの下に挿入することで通常のオーバーライドとsuperの呼び出しの技法が使えるようになる。
module EnthusiasticGreetings
def greet
"Hey, #{super}!"
end
end
class MyClass
prepend EnthusiasticGreetings # includeではなくprependを使用
def greet
"hello"
end
end
# EnthusiasticGreetingsがMyClassより下に挿入される
MyClass.ancestors[0..2] # => [EnthusiasticGreetings, MyClass, Object]
# EnthusiasticGreetings#greetが呼ばれ、superでMyClass#greetに委譲される
MyClass.new.greet # => "Hey, hello!"
まとめ
-
alias_method_chainは冗長なエイリアス設定を自動化するRails独自のメソッドで、アラウンドエイリアスを簡潔に表現するために使われていた - モジュールで同名メソッドを定義し
superで元実装に委譲すれば代用できるが、クラスに直接定義されたメソッドはincludeしたモジュールより継承チェーン上、下に来るためオーバーライドできない - Ruby 2.0で導入された
Module#prependは対象クラスの下にモジュールを挿入するため通常のオーバーライドが可能となり、alias_method_chainは役割を終えて廃止された
参考文献
この記事は以下の情報を参考にして執筆しました。