uri/queryform.rbの内部構成についてのメモ(delegate の利用例)

https://tociyuki.sakura.ne.jp/archive/uri/queryform.rb バージョン 0.03
クエリーの内容を保持しているハッシュへ、メソッドのほとんどを回して実行させています。今回は、メソッドをたらいまわしする先が 1 個だけなので、記述が簡潔になる添付ライブラリ delegate を使いました。
クラス定義の冒頭部分のややこしい部分は次の 3 段階を経て、QueryForm クラスを定義します。

  1. delegate ライブラリが提供する DelegateClass 関数でメソッドを Hash へたらい回しするクラス・オブジェクトを生成します。
  2. そのクラスのたらい回し先のアクセッサを private にし、再定義したいインスタンス・メソッドの削除をおこないます。
  3. いじり終わったクラス・オブジェクトのサブクラスとして QueryForm を定義します。
  class QueryForm < DelegateClass(Hash).instance_eval {
      private :__getobj__, :__setobj__
      [ :dup, :clone, :clear, :reject, :reject!, :delete_if,
        :merge, :merge!, :replace, :update, :to_s
      ].each {|method| remove_method method }
      self
    }

上の 2 の段階で、特に重要なのは dup と clone のメソッドの削除です。これをやっておかないと、QueryForm でいくら dup と clone を上書きしても期待した動作をしません。上書きしたメソッド定義の中で、super を使わないと複製処理がおこなわれませんが、DelegateClass 関数が自動的に生成する dup と clone は、self の複製ではなくインスタンス変数が束縛されているオブジェクトの複製を返すため、self の複製をつかまえることができなくなってしまうためです(delegate のバグでは)。

また、第 2 段階を instance_eval のブロックでおこなっているのは、private と remove_method がプライベート・インスタンス・メソッドだからです。このブロック実行中の self は、DelegateClass 関数が作成したデリゲート機能をもったクラス・オブジェクトに束縛されています。ブロックの戻り値を self にして、それを QueryForm のスーパークラスにします。

clone の定義は次のとおりです。

    def queries=(x)
      __setobj__(x)
    end
    
    protected :queries=
    
    def clone
      obj = super
      obj.queries = queries.clone
      obj
    end

__setobj__ インスタンス・メソッドは DelegateClass 関数が生成したもので、メソッドがデリゲートされるインスタンス変数を x へ変更するのに利用します。対になるのが、__getobj__ インスタンス・メソッドです。上の第2段階でプライベート・メソッドにしてしまっています。これを、プロパティ・ライタでラッピングし、このライタをレシーバ指定付きで利用するのは、clone と dup だけなので、protected へ。

他にも、Hash のメソッドのうち self を返す約束のメソッドも DelegateClass 関数が自動的に生成するものは利用できません。Hash のオブジェクトをリターン値にしてしまうからです。そのため、DelegateClass 関数が作ったメソッドを削除してから、self を返すようにメソッドを定義しなおしています。

    [:clear, :delete_if, :merge!, :replace, :update].each do |method|
      module_eval <<-END_DEF
        def #{method}(*a, &p)
          queries.#{method}(*a, &p)
          self
        end
      END_DEF
    end

recject、reject!、merge を Hash 通りの意味でリターン値を返すには、上のやりかたでは対応できないので別途定義しています。なお、reject と merge は本当は dup を使うべきかもしれませんが、new でごまかしました。