instance_evalとblock

Blockはクロージャを期待する場合と, 単なるblockスタイルで分かりやすく手続きを書きたいという2つの期待があると思ってる。(両方期待する場合もある)

1 get '/' do
2   "Hello Sinatra"
3 end

今日はSinatraようにblockスタイルで分かりやすく手続きを書きたい場合について書いていく。 大凡このような場合、blockを宣言したクラスはプロキシの責務を負うようなクラスで、実際blockを処理するのはサービス層に分類される別クラスだったりする。実現方法としてまず思いつくのはinstance_eval

 1 class Iga
 2   def kuchiyose &block
 3     Dragon.new("DragonFire").nin &block
 4   end
 5 end
 6 
 7 class Dragon
 8   attr_reader :waza
 9 
10   def initialize(waza)
11     @waza = waza
12   end
13 
14   def nin &block
15     instance_eval(&block)
16   end
17 end
1 Iga.new.kuchiyose do
2   "Ninpo: #{waza}" # => "Ninpo: DragonFire"
3 end

Iga忍者は口寄せの術を使い、龍を召喚しドラゴンフレイムの攻撃に成功している。口寄せなのでIgaクラスがドラゴンフレイムの保持はしておらず、Dragonクラスが保持する。 ポイントはIgaクラスのインスタンスで束縛されているblockがinstace_evalによってDragonクラスの環境で実行できている事だ。
こういうTipsを有名なライブラリは当たり前のように使っているが、なんせ名前付けされてないので読めはするものの、書くとなるとなかなか思いつかなかったりする。アプリケーションエンジニアとはこういうTipsを積み重ね自在にコードを操るスキルも必要だと思う。インフラエンジニアも巷で話題のフルスタックエンジニアもコードは書けるのだし。

hashのnest, nest, nest...

N階層でネストさせるhashの宣言はこんな感じに書くと良い

1 hash = Hash.new {|h,k| h[k] = Hash.new(&h.default_proc) }
2 hash[:get][:path] = '/' # => { get: {path: '/'} }

Hashは保持していないkeyが参照されるとHash.newに渡されたblockを実行する。トリックの種はdefault_procでこいつはレシーバのblockをprocとして返すので&で再度blockとしているだけ。これでN階層のhashを手軽に作成できる。

これ書いてて思い出したのがコロンブスの卵だ。
1942年、アメリカ大陸を航海中に発見したコロンブスを「ただ航海中にたまたま見つけただけじゃないか。」と批判する人々に対し、「卵を立てられるか」と問う。誰も立てる事ができなかったが、コロンブスは卵の尻を凹ませて、立てたせ一言。「何事も、後から種を明かされたら簡単に見えるものなのですよ。」

引数の重複

例えばこんなコード

 1 def csv_data(since_time, till_time)
 2   User.actice_user(since_time, till_time)
 3 end
 4 
 5 def data_count(since_time, till_time)
 6   csv_data(since_time, till_time).size
 7 end
 8 
 9 def run(since_time, till_time)
10   write_csv_file csv_data(since_time, till_time)
11 end

since_time,till_timeが引数として散在している。こんな場合はメジャーな策が主に2つあり、その判別法も割とシンプルだ。 まず考えるのが 重複しているデータがプレーンなデータか?何らか処理後(もしくは処理を行う予定の)のデータか? だ。

  • プレーンなデータの場合
    since_time,till_timeは他のクラスなどで生成されたデータで本クラスのメソッドに渡されているものとし、特に加工せず使い回すデータだとしましょう。そんなプレーンなデータである場合はデータをインスタンス変数として保持すると良いでしょう。具体的には
 1 def initialize(attr={})
 2   @since_time = attr[:since_time]
 3   @till_time  = attr[:till_time]
 4 end
 5 
 6 def csv_data
 7   User.actice_user(@since_time, @till_time)
 8 end
 9 
10 def data_count
11   csv_data.size
12 end
13 
14 def run
15   write_csv_file csv_data
16 end

インスタンス変数を使用した結果、見事に引数の重複はなくなりシンプルになりましたね。 特に加工せず使い回しているデータ というのが重要で、もし途中で処理し値が変化するようなデータだと、インスタンス変数で保持する前に少し踏みとどまって考えた方が良いでしょう。なぜならファイルが長くなった場合(それ自体がアンチパターンなのだが)などにどのタイミングでデータ値が変更されているか把握しづらいので、インスタンス変数の参照時点で、それはもう意図と異なる値かもしれません。

  • 何らか処理後(もしくは処理を行う予定の)のデータの場合
    since_time,till_timeが他のクラスから渡され、本クラスで処理を行った後使い回す場合、もしくは本クラスで処理によって求めたデータであるとしましょう。そのような何らか処理後(もしくは処理を行う予定の)のデータの場合はメソッド化すると良いでしょう。具体的には
 1 def csv_data
 2   User.actice_user(since_time, till_time)
 3 end
 4 
 5 def data_count
 6   csv_data.size
 7 end
 8 
 9 def run
10   write_csv_file csv_data
11 end
12 
13 def since_time
14    Date.today
15 end
16 
17 def till_time
18    Date.today - User.find_by(user_roul: 1, user_active: true).created_at
19 end

メソッドを使用した場合も同様に、重複なくシンプルにすることが出来ました。

1 def since_time
2    @since_time ||= Date.today
3 end
4 
5 def till_time
6    @till_time ||= Date.today - User.find_by(user_roul: 1, user_active: true).created_at
7 end

また場合にもよりますがsince_time,till_timeを取得するのにDBからの取得が伴ったり、重い処理で何度も実行させたくない場合は、この様にメモ化するのが一般的ですね。そもそもメソッド自体をなくしたりみたいな話になると魔術の世界になるので今日はここらでやめておく。

Proc.new(&block)という名の有難迷惑

いつもの如くgemリーディングを行っていたらこんなコードに出くわした

1 def define_route path, options, &block
2   self.route_defs << [path, options, Proc.new(&block)]
3 end

おやProc.new(&block)て意味ないのでは..

1 def define_route path, options, &block
2   self.route_defs << [path, options, block]
3 end

そもそも引数の&blockProcに変換しているので、この様にProc.newしなくても、そのまま変換されたものを渡せばいいのでは

1 Proc.new{ "kakuremi" }

Proc.newを使う場合はブロックを直接渡すか

1 def ninpo
2   Proc.new
3 end
4 
5 ninpo do
6   "kakuremi"
7 end

こんな感じでninpoに渡されたブロックを元にProcを作成するのであって、この場合はninpoの引数にブロックは必要ない。なので

1 def define_route path, options, &block
2   self.route_defs << [path, options, block]
3 end

もしくは

1 def define_route path, options
2   self.route_defs << [path, options, Proc.new]
3 end

でも良い。ただ引数にブロックを明示的に宣言した方が、親切なので後者より前者が良いと思います。
まぁでも先のコード書いた人の意図としては、Procオブジェクトであるのにblockという変数名で配列に収めるのが気持ち悪かったのだろう。分からなくないでもありません。しかしブロックはオブジェクトではないので変数名がblockである時点でそれはもうProcなのですよ。それに「事情があってProc.new(&block)で新たなオブジェクトを作ろうとしているのでは」とミスリーディングする恐れがある。

1 def ninpo &block
2   block.equal? Proc.new(&block) # => true
3 end
4 
5 ninpo do
6   "kakuremi"
7 end

残念ながらProc.newしても同じブロックが生成要素なら同じオブジェクトなのです。(rubyいいぞぉ〜〜〜)
そもそもProcよりlambdaのほうが好みだ。もっというなら->の方がかっこいい。脱線乙。

Rvineっていうgem作った

Rvineっていうgem作りました。 RvineはVine APIのruby wrapperです。 github: Kyuden/rvine

感性の豊かさがすばらしいっすね。動画作ってアップしないだろうけど、見る分には楽しいです。日本人の投稿は少ないのですが、それがまた良かったり。 Vineをリソースとしたバイラルメディアサービスをrubyで作りたい人はRvine使ってみては如何ですか?機能が足りなければIssueにあげてください。

僕は「◯◯を作った」というエントリーが大好きで、そこに価値を感じる性分なので、ちょこちょこと作っては公開していきたいす。にしても自分のgemがrubygems.orgからインストールできるのは地味に嬉しいっすね〜。ToDo:spec

1 gem install rvine

f:id:kyuden:20140415202155p:plain

my version.rb

gemのversion.rbです

 1 module M
 2   class Version
 3     MAJOR = 0
 4     MINOR = 9
 5     PATCH = 3
 6     PRE = nil
 7 
 8     class << self
 9       def to_s
10         [MAJOR, MINOR, PATCH, PRE].compact.join('.')
11       end
12     end
13   end
14 end

スキーマポエム

ファイルがディレクトリ上に属する様に、テーブルも何れかのスキーマに必ず属します。ディレクトリごとに書込みなどの権限が設定出来るように、スキーマにも属するテーブルの権限を設定する事ができます。ディレクトリ内に同名のファイルを配置出来ないように、あるスキーマに同名のテーブルは属せません。ディレクトリが違えば同名のファイルは配置出来るように、スキーマが違えば同名のテーブルを作成する事ができます。

よって、スキーマにより単一のDB内に複数のアプリの環境を作成する事が可能となります。なぜなら、アプリごとにテーブルをスキーマで区切り、スキーマごとに権限を設定できるからです。

インスタンス変数生成のタイミング

インスタンス変数は値が代入された時に初めて生成されます。 なので同じクラスのオブジェクトでもインスタンス変数の数が異なることが十分にあり得ますね

 1 class MyClass
 2   def create
 3     @test = "hello"
 4   end
 5 end
 6 
 7 obj = Myclass.new
 8 p obj.instance_variables
 9 p obj.create
10 p obj.instance_variables
[]
"hello"
[:@test]

Push All find() Calls into Finders on the Model

rails_antipatternsについて書いていきます

<html>
<body>
  <ul><% User.find(:order => "last_name").each do |user| -%>
      <li><%= user.last_name %> <%= user.first_name %></li>
    <% end %>
  </ul>
</body>
</html>
  • View内で直接DBにアクセスすべきでない

[Try1] move the logic into the Controller

1 class UsersController < ApplicationController
2   def index
3     @users = User.order("last_name")
4   end
5 end
<html>
<body>
  <ul>
    <% @users.each do |user| -%>
      <li><%= user.last_name %> <%= user.first_name %></li>
    <% end %>
  </ul>
</body>
</html>

AntiPattern

  • DBのfinderはModelで記述すべき

[Try2] move the direct find call down into the Model

1 class UsersController < ApplicationController
2   def index
3     @users = User.ordered
4   end
5 end
1 class User < ActiveRecord::Base
2   def self.ordered
3     order("last_name")
4   end
5 end

railsではさらに美しく記述できる機能が用意されている

[try3] use the named scpoe of rails

1 calss User < ActiveRecord::Base
2   scope :orderd, order("last_name")
3 end

ちょーきもちー

Keep Finders on Their Own Model

rails_antipatternsについて書いていきます

1 class UsersController < ApplicationController
2   def index
3     @user = User.find(params[:id])
4     @memberships = @user.memberships.where(:active => true).
5                                      limit(5).
6                                      order("last_active_on DESC")
7   end
8 end

Solution: Push All find() Calls into Finders on the Modelを適用してみよう

[try1] Solution: Push All find() Calls into Finders on the Model

1 class UsersController < ApplicationController
2   def index
3     @user = User.find(params[:id])
4     @recent_active_memberships = @user.find_recent_active_memberships
5   end
6 end
1 class User < ActiveRecord::Base
2   has_many :memberships
3 
4   def find_recent_active_memberships
5     memberships.where(:active => true).
6                 limit(5).
7                 order("last_active_on DESC")
8   end
9 end

AntiPattern

UserModelでMembershipModelのfinderを記述すべきでない。 自身のModelのドメインを明確に。

[try2] move Membership Model finder into the Membership Model From the User Model

1 class User < ActiveRecord::Base
2   has_many:memberships
3 
4   def find_recent_active_memberships
5     memberships.find_recently_active
6   end
7 end
1 class Membership < ActiveRecord::Base
2   belongs_to :user
3 
4   def self.find_recently_active
5     where(:active => true).limit(5).order("last_active_on DESC")
6   end
7 end

where(:active => true)とかorder("last_active_on DESC")のクエリメソッドは他でも結構使えそう

[try3] use the named scope of rails

1 class User < ActiveRecord::Base
2   has_many :memberships
3 
4   def find_recent_active_memberships
5     memberships.only_active.order_by_activity.limit(5)
6   end
7 end
1 class Membership < ActiveRecord::Base
2   belongs_to :user
3   scope :only_active, where(:active => true)
4   scope :order_by_activity, order('last_active_on DESC')
5 end

すっきりー



© 2015 kyuden