「Rubyベストプラクティス」を読む3


3章の話は、2章と似たような優れたインターフェイスの作り方やRubyのリフレクションが紹介されています。

3章目次

  • BlankSlate:ステロイドで強化されたBasicObject
  • 柔軟なインターフェイスを作る
    • instance_eval()をオプション化する
    • method_missing()とsend()を使ってメッセージを扱う
    • 2つの目的を兼ねたアクセサ
  • オブジェクトごとの振る舞いを実装する
  • 既存のコードを拡張、変更する
    • 新しい機能を追加する
    • エイリアス経由で変更する
    • オブジェクトごとの変更
  • クラスとモジュールをプログラムで作る
  • フックとコールバックを登録する
    • 新しく追加された機能を検出する
    • 継承を補足する
    • Mix-inを補足する

BlankSlate:ステロイドで強化されたBasicObject

BlankSlateというライブラリの紹介。メソッドを公開、非公開する仕組みをもったクラス。
仕組みは、メソッドを非公開にするときは、Module#instance_methodで、UnboundMethodを取得しておいてから、undef_methodでメソッドを削除する。
メソッドを公開するときは、取得しておいたUnboundMethodをModule#define_methodに与えて、メソッドを定義する。

柔軟なインターフェイスを作る

instance_eval()をオプション化する

2章で紹介した、instance_evalを使ってselfを変えるAPIはカッコイイけど、selfが変わることが都合が悪い時がある。

class Document
  def self.generate(file, &block)
    d = Document.new
    d.instance_eval(&block)
    d.write(file)
  end

  def text(arg)
    # 何かする
  end
end

class Foo
  def bar
    "bar"
  end

  def use_document
    Document.generate("test.txt") do
      # selfがDocumentオブジェクトなので、下のFoo#bar呼び出しは使えない
      text "my document #{bar}"
    end
  end
end

こういうときは、ブロックに与えられた引数の個数を調べて、instance_evalを使うか、自身のオブジェクトを引数として与えるか
選択するようなコードを書ける。

class Document
  def self.generate(file, &block)
    d = Document.new
    block.arity < 1 ? d.instance_eval(&block) : block.call(d)
    d.write(file)
  end
end

class Foo
  def use_document
    Document.generate("test.txt") do |d|
      d.text "my document #{bar}"
    end
  end
end
method_missing()とsend()を使ってメッセージを扱う

method_missingでメソッド名を解析して、解析した文字列を、sendでメソッド呼び出しする例。
例えば、fill_and_strokeというメソッド呼び出しをmethod_missingでフックして、fillとstrokeのメソッドを呼び出すとか。
ActiveRecordのfind_by_*** とかと同じような仕組みの話だとおもう。

2つの目的を兼ねたアクセサ

メソッドに引数があれば、セッターとして動作して、引数がなければゲッターとして動作するメソッドの話。
これが必要になるのは、instance_evalを使ったAPIを提供しようとするとき、=の付くセッターでは、ローカル変数とみなされてしまうから。

Document.generate do
  # selfを指定しなければいけない
  self.font_size = 10
  text "the font size is #{font_size}"
end

# セッター、ゲッターとして動作するメソッドを作る
class Document
  def font_size(size=nil)
    return @font_size unless size
    @font_size = size
  end
end

Document.generate do
  # selfを指定しなくても書ける
  font_size(10)
  text "the font size is #{font_size}"
end

オブジェクトごとの振る舞いを実装する

特異クラスを使って、オブジェクト固有のメソッドを作る話

user = User.new
# 特異クラスの取得
singleton = class << user; self; end
# メソッド定義
singleton.__send__(:define_method, :logged_in?){ true }

既存のコードを拡張、変更する

新しい機能を追加する

オープンクラスなので、メソッドを追加で定義できるという話。

エイリアス経由で変更する

メソッドに機能を追加したいとき、Module#alias_methodを使ってメソッドの名前を変えて、追加の機能を実装した同名メソッドを作れる。

オブジェクトごとの変更

オブジェクトをextendして追加の機能を持たせることができる。クラス単位の変更となるModule#alias_methodと違って、こちらの方法はオブジェクトごとの変更になる。

クラスとモジュールをプログラムで作る

Class#newで匿名クラスを作れる。Class#newのブロック内でdefすれば、匿名クラスのインスタンスメソッドを作成できる。

def Mystery(secret)
  if secret == "chunky"
    Class.new do
      def message
        "You"
      end
    end
  else
    Class.new do
      def message
        "Don't"
      end
    end
  end
end

class Win < Mystery("chunky")
  def who_am_i
    "I am Win"
  end
end

class EpicFail < Mystery("smooth ham")
  def who_am_i
    "I am teh fail"
  end
end

フックとコールバックを登録する

新しく追加された機能を検出する

メソッド定義をmethod_addedでフックできる。

継承を補足する

継承はinheritedでフック出来る。

Mix-inを補足する

includeはincludedでフック出来る。本でも紹介されているけど、以下のようなincludedの使い方を結構見かける気がする。

module MyModule
  module ClassMethods
    def class_method
      # クラスオブジェクト用のメソッド
    end
  end

 def foo
    # インスタンス用のメソッド
  end

  def self.included(base)
    base.extend(ClassMethods)
  end
end

class Foo
  include MyModule
  # includedのおかげで、下のようにextendを書く必要はない
  # extend MyModule::ClassMethods
end

まとめ

章の最後では、この章で紹介されたテクニックを使ったNaiveCampingRoutesという面白いコードが書いてある。