メタプログラミングRuby第2版 Ⅰ部を読んだの続き
メタプログラミングRuby第2版 Ⅱ部を読んだので、簡単な内容と感想を書いていく。
最後に、読んでメモしたものを追記しておく。
I部ではRubyのメタプログラミングの考え方や継承チェーンの仕組み、メソッドやイディオムの紹介が主な内容となっていた。
Ⅱ部では、Ruby on Rails に含まれるライブラリである Active Record や Active Support などがメタプログラミングの技術を用いてどうやって実装されているかを学ぶ。
個人的に面白かったのは、第10章のActive SupportのConcernモジュールで、includeとextendメソッドを使ったトリックのメリットとデメリットをあげ、Concernモジュール内ではそのデメリットをどう解決しているかを実際のコードを読みながら学ぶことができる部分だ。
といっても、実際に取り上げられるモジュールのコードはほんの1ファイルの一部だけなので、Railsの学習も兼ねて、ActiveSupportやActiveModelあたりのモジュールはざっくりコードリーディングしていきたいところ。
あと、付録のCが「魔術書」となっていて、メタプログラミングのトリックやイディオムが33個書かれている。
ここを読むだけでも、この本の価値はあるんじゃないかな。
メタプログラミングRuby第2版は全体的に内容も面白いので、少しでも興味があるなら読むことをおすすめします!
第9章 Active Recordの設計
- オートローディング
- ActiveSupport::Autoloadモジュール
- モジュール名を最初に使ったときに、自動的にモジュール(やクラス)のソースコードを探して、requireするという命名規約が使われている
- Active Record は ActiveSupport::Autoload をエクステンドしているので、autoload は ActiveRecord モジュールのクラスメソッドになる(わからない場合は、クラス拡張(p.135)を読む)
- run_load_hooks を呼び出している行がある。これは、オートロードされたモジュールが設定用のコードを呼び出せるようにするもの
- Validationsモジュール
- validateメソッドは、ActiveModel::Validationsにある
- ActiveRecord::Validationsがインクルードしたモジュール
- クラスがモジュールをインクルードすると、通常はインスタンスメソッドが手に入るが、validate は ActiveRecord::Base のクラスメソッドである
第10章 Active SupportのConcernモジュール
- Kernel#autoload
- includeとextendのトリックは便利ではあるが、問題もある
- クラスメソッドを定義するあらゆるモジュールが、インクルーダーを拡張するincludedというフックメソッドを定義する必要が出てくる
- includeの連鎖の問題
- インクルードするモジュールが、また別のモジュールをインクルードしている場合
module SecondLevelModule
def self.included(base)
base.extend ClassMethods
end
def second_level_instance_method; 'ok'; end
module ClassMethods
def second_level_class_method; 'ok'; end
end
end
module FirstLevelModule
def self.included(base)
base.extend ClassMethods
end
def first_level_instance_method; 'ok'; end
module ClassMethods
def first_level_class_method; 'ok'; end
end
include SecondLevelModule
end
class BaseClass
include FirstLevelModule
end
BaseClass.new.first_level_instance_method
BaseClass.new.second_level_instance_method
BaseClass.first_level_class_method
BaseClass.second_level_class_method
- Rails2だと上記のコードの問題を以下のように解決している
module FirstLevelModule
def self.included(base)
base.extend ClassMethods
base.send :include, SecondLevelModule
end
...
end
- ActiveSupport::Concernは、
includeとextendのトリック
をカプセル化して、includeの連鎖の問題を解決している
- この機能を手に入れるには、モジュールでConcernをextendして自身のClassMethodsモジュールを定義する
require 'active_support'
module MyConcern
extend ActiveSupport::Concern
def an_instance_method; " インスタンスメソッド "; end
module ClassMethods
def a_class_method; " クラスメソッド "; end
end
end
- モジュールがConcernをextendすると、extendedを呼び出す。エクステンダーにクラスインスタンス変数(p.114)である@_dependenciesを定義する
- Module#append_features は、Ruby のコアのメソッド
- includedとappend_featuresの違い
- includedはフックメソッドなため、通常はメソッドの中身がなく、オーバーライドして使う
- append_featuresは実際にインクルードするもの
- インクルードされたモジュールがインクルーダーの継承チェーンに含まれているかどうかを確認して、含まれていなければ継承チェーンにモジュールを追加する
- append_features をオーバーライドすると、モジュールが一切インクルードされなくなる。
- 本章では、「ActiveSupport::Concern をエクステンドしたモジュール」を表すときに、小文字 の「concern」を使うことにする。上記のコードでは、MyConcern が concern だ。今の Rails では、ActiveRecord::Validations や ActiveModel::Validationsも含めて、ほとんどのモジュールがconcernである。
rails/activesupport/lib/active_support/concern.rb
module Concern
class MultipleIncludedBlocks < StandardError
def initialize
super "Cannot define multiple 'included' blocks for a Concern"
end
end
def self.extended(base)
base.instance_variable_set(:@_dependencies, [])
end
def append_features(base)
if base.instance_variable_defined?(:@_dependencies)
base.instance_variable_get(:@_dependencies) << self
false
else
return false if base < self
@_dependencies.each { |dep| base.include(dep) }
super
base.extend const_get(:ClassMethods) if const_defined?(:ClassMethods)
base.class_eval(&@_included_block) if instance_variable_defined?(:@_included_block)
end
end
def included(base = nil, &block)
if base.nil?
if instance_variable_defined?(:@_included_block)
if @_included_block.source_location != block.source_location
raise MultipleIncludedBlocks
end
else
@_included_block = block
end
else
super
end
end
def class_methods(&class_methods_module_definition)
mod = const_defined?(:ClassMethods, false) ?
const_get(:ClassMethods) :
const_set(:ClassMethods, Module.new)
mod.module_eval(&class_methods_module_definition)
end
end
- Concernの基本的な考え方。ActiveSupport::Concern をエクステンドしたモジュールの中で、ActiveSupport::Concernをエクステンドしたモジュールをインクルードしない
- ActiveSupport::Concernをエクステンドしたモジュールでないモジュールに別のActiveSupport::Concern をエクステンドしたモジュールがインクルードされたら、すべての依存関係をインクルーダーに一気に流し込む。
- このスコープのなかでは、self は ActiveSupport::Concern をエクステンドしたモジュール である。base はインクルードしているモジュール。
- concern かもしれないし、そうではないかもしれない。
- append_features 内では、インクルーダーがActiveSupport::Concern をエクステンドしたモジュールかどうかを確認したい。クラス変数 @_dependencies があれば、それが ActiveSupport::Concern をエクステンドしたモジュール だとわかる。
- インクルーダーが ActiveSupport::Concern をエクステンドしたモジュール ではない場合(たとえば、ActiveRecord::Validations が ActiveRecord::Base にインクルードされた場合)は、何が起きるのだろうか? ここでは、他の ActiveSupport::Concern をエクステンドしたモジュール がインクルードされるなどして、すでにインクルーダーの継承チェーンに自身が追加され たかどうかを確認している(base < selfが意味するのはそういうことだ)
- 継承チェーンに追加されていない場合
- インクルーダーに依存関係を再帰的にインクルードしていく。この最小主義の依存管理システムが「10.1.2 include の連鎖の問題」で触れた問題 を解決する。
- super で Module.append_features を呼び出して、継承チェーンに自分自身 を追加している
- ClassMethods の参照は Kernel#const_get を使って取得する必要がある。 コードが物理的に配置されている Concern モジュールではなく、self のスコープで定数を読み込まなければいけないから。
11章 alias_method_chain の盛衰
- 名声を極めたalias_method_chain メソッドが不評 を招き、最終的に Rails のコードベースから姿を消した話
- alias_method_chain はエイリアスの重複を取り除くことができるが、それ自身に問題がある。 alias_method_chain はアラウンドエイリアス(p.140)をカプセル化したもの
- Rails のなかでメソッドのリネームとシャッフルを繰り返したことで、実際に呼び出して いるメソッドがどのバージョンかを追跡するのが難しくなってしまった
12章 アトリビュートメソッドの進化
- アクセサを動的に定義せず、ゴーストメソッドだけを使う
- オブジェクトを生成したときに initialize メソッドでアクセサを定義する
- アクセスされたアトリビュートのアクセサだけを定義する
- 派生フィールドも含めて、オブジェクトのすべてのアクセサを常に定義する
- コード文字列ではなく、 define_method でアクセサを定義する