メタプログラミングRuby第2版 Ⅱ部を読んだ

メタプログラミング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 # => "ok"
BaseClass.new.second_level_instance_method # => "ok"
BaseClass.first_level_class_method # => "ok"
BaseClass.second_level_class_method # => NoMethodError
  • 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 #:nodoc:
    def initialize
      super "Cannot define multiple 'included' blocks for a Concern"
    end
  end

  def self.extended(base) #:nodoc:
    base.instance_variable_set(:@_dependencies, [])
  end

  def append_features(base) #:nodoc:
    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

  # Evaluate given block in context of base class,
  # so that you can write class macros here.
  # When you define more than one +included+ block, it raises an exception.
  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

  # Define class methods from given block.
  # You can define private class methods as well.
  #
  #   module Example
  #     extend ActiveSupport::Concern
  #
  #     class_methods do
  #       def foo; puts 'foo'; end
  #
  #       private
  #         def bar; puts 'bar'; end
  #     end
  #   end
  #
  #   class Buzz
  #     include Example
  #   end
  #
  #   Buzz.foo # => "foo"
  #   Buzz.bar # => private method 'bar' called for Buzz:Class(NoMethodError)
  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 でアクセサを定義する