woshidan's loose leaf

ぼんやり勉強しています

Builderパターン

もうお前は何回ActiveRecordを取り上げるんだ、という感じですが、これで一旦最後じゃないですかね。 あるいは、この例はAndroidのNotificationBuilderでもいいんですが。

Builderパターンについて、ここで使うといいなと思っている場面が二つあります。

  • 任意の引数、処理が非常に多いとき
  • 構造は一緒でも成果物の詳細を変えたいとき

任意の引数、処理が多いとき

これについては、Androidの以下の記事を見ていたときからずっと思ってました。

http://qiita.com/KeithYokoma/items/c5c1c01cf695e6bf703f

インスタンスを作る際、たくさんの必ずしも設定しなくても良いパラメータがあり、 それらをすべて設定したり、すべてハッシュ等で隠して渡したり、といったことをするのが大変、という場合、 必須のパラメータだけを受け取るコンストラクタを作成しておいて、 任意のパラメータはビルダークラスを通して渡す、というものです。

class User
  attr_accessor :name, :age, :address, :class
  def initialize(name, age, address, pramas = {})
    @name = name
    @age = age
    @address = address

    # paramsに何が入っているか分からないので少しプログラミングミスするとすぐにエラーが発生する可能性がある
    @class = params[:class]
    ...
    ...
  end

  ...
end

class User
  attr_accessor :name, :age, :address, :class
  def initialize(name, age, address)
    @name = name
    @age = age
    @address = address
    # ここか、Builderのinitializeにデフォルト値を入れる処理を書く
    @class = 1 # デフォルト
    ...
  end

  class Builder
    def initialize(name, age, address)
      @user = User.new(name, age, address) 
    end

    def setClass(class)
      @user.class = class
    end

    ...
  end
  ...
end

ちなみに読んでて個人的に一番困るパターン

def function(params = {})
  @name = params[:name].to_s # to_sでnilガードをするという意識はある
  send(params[:method]) # え...
  ...
  ...
  # paramsに何が入っているか分からないけれど、
  # 必須のパラメタがいくつかあり、読み込まないとそれが分からない...
  # 割とめんどくさい
end

この方法の利点は、どのパラメータが設定されていないか、設定されているか、がはっきりするところで、 たくさんパラメータを使いたい場合は少し大変ですが、思わぬところで思わぬ値が入っていてデバッグしにくいエラーが発生する、 という事態を防ぐ事が出来ます。

その、たまに、設定する変数を忘れてnilのままインスタンスの初期化処理を抜けて、そのインスタンスを利用する処理でundefined method xxx For nil:NilClassが結構多い時とかは、考えてみるといいのではないでしょうか。

あと、あまりにもたくさんのパラメータを1つのインスタンスで何度も扱いたい場合は、 何かが間違っているので素直にクラスを分割したらいいのでは、という感じです。

構造は一緒でも成果物の詳細を変えたいとき

なんでもありそうですが、ActiveRecordが「DirectorクラスがBuilderクラスしか知らないことの利点」の例として分かりやすかったので例として上げておきます。

ActiveRecordのQueryMethodsあたりがBuilder役で、ArelかActiveRecordの各言語のAdapterないし、Adapterが前提としているgemがConcreteBuilderに当たるとします(実際にSQLを作っている部分がConcreteBuilderとすると正確なところはよく分かんなかったです...)。

私たちがActiveRecordを利用して任意のSQLを発行するスコープ等(Director役)を書く時、QueryMethodsに書いてある、where, not, group, selectなどのメソッドを知っていればよいです。 (この場合は、Controllerなどからモデルのメソッドを利用するとき、ControllerがClientとしておきましょうかね...。Compositeパターンでもあるから話がややこしい!)

いくつwhere節に条件を追加していくかはwherechainを利用して指定する事が出来ますが、 いまコードを書いているRailsアプリケーションがどのデータベースに接続しているかを意識する必要はありません。 (というか、かなりコード読んでいかないと任意の箇所で切り替えたりは難しいし、するものでもありません)

Builderクラスのメソッドに基づいて具体的にどのようにSQLが発行されるかは、ConcreteBuilderクラスの関心事だからです。

だから、環境間での移植性がとても高く、テスト環境ではSQLite3, 開発環境ではMySQL, 本番環境ではPostgresqlなんて事が出来たりします。

ActiveRecordを書けなくてもいいと思うんですが、このへんのこと忘れてActiveRecordしか叩けない身体になるのは怖いですね。

ActiveRecord::QueryMethods https://github.com/rails/rails/blob/80f011ea542c801afc852e1a5f9efa9a30dd4219/activerecord/lib/active_record/relation/query_methods.rb

( 本当はActiveRecordのどのへんでSQLが作られているかもうちょい調べたかったのですが、 調べてたら40くらいのクラスをルックアップしてるとか書いてあって心折れた......。

https://practicingruby.com/articles/implementing-the-active-record-pattern-1

なんか時間が出来たときに上ら辺でも流し読みしよう......。 )

また、ActiveRecordは1つ目に上げた「任意の引数、処理が多いとき」の例としてもとてもよくて、 必要な分だけwhereメソッド等を足していくので、単純なものを単純に、複雑なものを複雑に、ありのままに書けます。

Builderパターンに対して、Builderパターンと気づけないとどうなるか

自分も前科があり、またRails大好きな人もたまにやっているような感じなのですが。

ActiveRecordはORマッパー(オブジェクトをデータベースに格納可能な単純な値のグループに変換する、データベースからレコードを取り出してオブジェクトの形に変換するクラス http://ja.wikipedia.org/wiki/%E3%82%AA%E3%83%96%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E9%96%A2%E4%BF%82%E3%83%9E%E3%83%83%E3%83%94%E3%83%B3%E3%82%B0)でもあるんですが、SQLビルダーでもあります。

SQLビルダーであるという側面が無視されているような気がしていて、ActiveRecordに突っ込むためのSQLを作るためのメソッドを延々と書いてしまう初心者がいます。

私もそうでした。

何が厄介かというと、このオリジナルビルダークラスはコードは可読性に欠ける上に移植性が低いことが多いのです。

それ、500行くらいで気合い入れて書いてあるけど、たぶん100行以内の普通のメソッドで書けるよ、ということが起こるんですよね...。

止めましょう。気をつけましょう。ActiveRecord自体がBuilderなんです。

Builderに対して、さらにBuilderを作ると、どこからどこまでwhereチェーンでクエリを作っていけば良いのか分からなくなってきます。

そのクラスがどんな役割を持っているか察せるようにAPIを眺めて、コードを眺めてから使いましょう。

読み込む必要までは無いので!

あと、 N + 1 問題についてはですね。includesなどで、後でviewで使うモデルを読み込んで置くと少しはマシになるかな、という理解です... (http://ruby-rails.hatenadiary.com/entry/20141108/1415418367)

でも、これデータ複雑になってきたらincludesじゃ済まないんですよね...( N+1問題 + 無駄ループで10秒以上処理時間が伸びていたケースがあった... )

そう言う場合に備えて大人しくSQLも書けるように備えておいた方がいいんじゃないですかね。