MUGENUP勉強会を開催しました!
こんにちは、MUGENUPの伊藤です。
今回は先日2/27日に開催した勉強会をまとめてみました!
当日は外部の方を6人お招きし、全部で12人での開催となりました。
開催までの流れ
MUGENUPの開発部では隔週木曜日に勉強会を開催しています。 元々は少人数でそれぞれの人が好きなことを発表するスタイルで、発表中にツッコミとかしながら和気あいあいとやっていました。
また、年末に外部の人を呼んでやったところ好評で、その後定期的に参加してくれるようになった人もいます。
そんなこんなで「本格的に外部の人を呼んでやってみたい!」と思い、元々MUGENUPと御縁のあったエンジニアの方や、知り合いづてなどで何人かに声をかけてみました。
すると皆さん快諾して頂き、晴れてこの勉強会開催にこぎつけました。
会場
今回はMUGENUPの入っているビルにある「シアタールーム」というところで開催しています。 名前の通り映画鑑賞や、カラオケなんかもできちゃう部屋なんですがソファでくつろげたり照明をいじれたりで結構雰囲気を出せるような部屋になっています。
当日の内容
当日は19:30 ~ 21:30の2時間で予定していました。 普段は一人あたり5~10分の発表で、発表中にツッコミを入れたりするので一人あたりだいたい15分くらいでのんびりやっていました。
しかし、段々と参加者が増えていき、「あれ、これそもそも発表時間オーバーするかも」などと思っていたのですが、 最終的には発表者が10名程だったことからなんとかギリギリ全員が発表できました。
さて、気になる当日の発表内容ですが、
vagrant, chef, knife soloを使ってmunin-nodeを複数サーバーに展開し監視をするまで
酔っぱらいレベルを測定できるアプリNomBay
Semantic Versioningについて
psd のファイル構造、または photoshop plugin を作る的な何か
楽しくなるPerl開発環境
RubyのSymbolについて
電子マネーに関する何か
binding.pryに隠れている黒魔術について
といった内容の発表がありました。
各人が好きなことを発表し合った結果、内容がかなりばらつき 普段なら接することはない技術をお互いが知ることができてかなり刺激的な勉強会になりました。
ちなみに私もperlは使ったことがなかったので、その後インストールして色々いじってみました。
社内では「また外部の人を呼んで勉強会をやろう!」という風に盛り上がったので、 また近いうちに開催予定です。
ご興味の有る方は伊藤までご連絡ください!
宣伝
MUGENUP では、知りたがりなエンジニアを募集中です。
無限流開発、ご一緒しませんか?
エントリーはこちら http://mugenup.com/recruit/information#web_application
Rails 4.1 の spring で paralell_tests を使う方法
みなさん、こんにちは! 2週間ぶりのご無沙汰、MUGENUP の osada です。
ruby 2.1.0
、Rails 4.1
で開発した、みなゲー編集部が正式リリースとなりました!
よろしくお願いします。
さて、そんなRails 4.1
の新機能として、プレローダーspring
が標準装備となりました。
本日はspring
とparalell_tests
を併用する方法についてのお話です。
要旨は下記となります。
PARALLEL_TESTS_EXECUTABLE
を設定して、paralell_tests
にspring
を使わせないbin/rspec
を書き換えて、始めのプロセスだけspring
を使うFailuresLogger
を使って、失敗したテストを再実行するRuntimeLogger
を使って、テストのグループを平均化する
では、よろしくお願いします。
PARALLEL_TESTS_EXECUTABLE
を設定して、paralell_tests
にspring
を使わせない
Rails 4.1
のテストで、rspec
では成功するのに、paralell_tests
では多くが失敗する、という現象に遭いました。
spring
が原因と考えられます。
通常、rake
やrspec
コマンドを使うとき、コード(code
)を読み込んで、メモリ上にRails
のapp
インスタンスを生成し、使用します。処理が終わると、app
は解放されます。
この処理はかなり重い処理なので、毎回作るのではなく、一度作ったapp
を使いまわそう、というのがspring
の動きです。spring
で作られたapp
は処理が終わっても維持され、再度使用されます。
このspring
はbin_stub
として提供され、bin/
というディレクトリに
rails
, rspec
, rake
のコマンドが用意されます。例えば、
be bin/rake db:create
のように、通常のコマンドの代わりに、bin/
下のコマンドを呼び出すことで、spring
が使用されます。
一方、paralell_tests
というのは、rake
などの処理を複数のプロセスに分けて同時に処理する方法です。
例えば、parallel:spec
を実行すると、下記の4つのコマンドが実行されます。
※
be
はbundle exec
のalias
です
$ be rake parallel:spec TEST_ENV_NUMBER=;export TEST_ENV_NUMBER;PARALLEL_TEST_GROUPS=4;export PARALLEL_TEST_GROUPS;rspec TEST_ENV_NUMBER=3;export TEST_ENV_NUMBER;PARALLEL_TEST_GROUPS=4;export PARALLEL_TEST_GROUPS;rspec TEST_ENV_NUMBER=4;export TEST_ENV_NUMBER;PARALLEL_TEST_GROUPS=4;export PARALLEL_TEST_GROUPS;rspec TEST_ENV_NUMBER=2;export TEST_ENV_NUMBER;PARALLEL_TEST_GROUPS=4;export PARALLEL_TEST_GROUPS;rspec
しかし、これにspring
を使用すると、4つのプロセスがあるのに、実際に稼働するapp
は1つになってしまいます。
よって、テストが失敗してしまうのです。
原因はparalell_tests
がbin/rspec
を使用するという点にあります。これをどうにかしましょう。
$ be rake parallel:spec TEST_ENV_NUMBER=;export TEST_ENV_NUMBER;PARALLEL_TEST_GROUPS=4;export PARALLEL_TEST_GROUPS;bin/rspec ……
結論からいうと、PARALLEL_TESTS_EXECUTABLE
を設定することで、bin/rspec
の使用を回避できます。
PARALLEL_TESTS_EXECUTABLE="bundle exec rspec" be rake parallel:spec TEST_ENV_NUMBER=;export TEST_ENV_NUMBER;PARALLEL_TEST_GROUPS=4;export PARALLEL_TEST_GROUPS;bundle exec rspec ……
この理由は、parallel_tests
は、実行するファイルを下記のように設定しているからです。
def executable ENV['PARALLEL_TESTS_EXECUTABLE'] || determine_executable end
またrspec
のdetermine_executable
は下記であり、[bin/rspec
, script/spec
, bundle exec rspec
, spec
, rspec
] のどれかを起動する事がわかります。
def determine_executable cmd = case when File.exists?("bin/rspec") "bin/rspec" when File.file?("script/spec") "script/spec" when ParallelTests.bundler_enabled? cmd = (run("bundle show rspec-core") =~ %r{Could not find gem.*} ? "spec" : "rspec") "bundle exec #{cmd}" else %w[spec rspec].detect{|cmd| system "#{cmd} --version > /dev/null 2>&1" } end cmd or raise("Can't find executables rspec or spec") end
bin/rspec
を削除しても良いのですが、それではspring
が使えなくなるので、
PARALLEL_TESTS_EXECUTABLE
を設定する方が適切でしょう。
以上、本項の結論は、
parallel_tests
に、spring
を使わせないためには、PARALLEL_TESTS_EXECUTABLE
を設定する となります。
bin/rspec
を書き換えて、始めのプロセスだけspring
を使う
しかしせっかくのspring
です。
4つの内1つだけでspring
を使い、
残り3つを別のプロセスで動かせば良いのではないでしょうか?
相手はbin_stub
ですから、修正はとても簡単です。
bin/rspec
のこれを、
if !Process.respond_to?(:fork) || Gem::Specification.find_all_by_name("spring").empty? exec "bundle", "exec", "rspec", *ARGV else ARGV.unshift "rspec" load Gem.bin_path("spring", "spring") end
こうしましょう!
if !Process.respond_to?(:fork) || Gem::Specification.find_all_by_name("spring").empty? exec "bundle", "exec", "rspec", *ARGV else if ENV["TEST_ENV_NUMBER"].nil? || ENV["TEST_ENV_NUMBER"].empty? ARGV.unshift "rspec" load Gem.bin_path("spring", "spring") else exec "bundle", "exec", "rspec", *ARGV end end
ENV["TEST_ENV_NUMBER"].nil?
は通常のbin/rspec
起動時です。
ENV["TEST_ENV_NUMBER"].empty?
は、parallel_tests
の一番始めのプロセスの時です。
つまり、
bin/rspec
として起動したとき、または、parallel_tests
の一番始めのプロセスの時に、spring
を使用する。
というbin_stub
になりました。
さて、肝心のスピードアップですが、feature
テストなどが多いため、私のプロジェクトでは違いが現れませんでした。
参考までとはなりますが、環境の影響が少ないspec/model
に適用した結果を載せておきます。
[feature/parallel stash]~/projects/mugenup/workstation: time RAILS_ENV=test PARALLEL_TESTS_EXECUTABLE="bundle exec rspec" bundle exec rake parallel:spec[model] …… 444 examples, 0 failures Took 216.680576 seconds real 3m47.661s user 3m20.585s sys 0m25.249s
[feature/parallel stash]~/projects/mugenup/workstation: time RAILS_ENV=test bundle exec bin/rake parallel:spec[model] …… 444 examples, 0 failures Took 212.844729 seconds real 3m37.395s user 0m1.378s sys 0m0.285s
real
を見ると10 sec
ほど速くなっているように見えますが、誤差の範囲かもしれません。
もし「速くなったよ!」という方がいらっしゃいましたら、ご一報ください!
FailuresLogger
を使って、失敗したテストを再実行する
feature
テストは実際のブラウザの挙動を再現するため、
タイムアウトなどの理由により、
タイミングによって失敗することもあります。
parallel_tests
を使うと、さらに失敗しやすくなります。
たまたま失敗したのか、本当に失敗しているのかを確認するため、
失敗したテストのみを再度実行するのですが、
その時に役立つのが、FailuresLogger
です。
.rspec_parallel
に paralell_tests
のみのオプションを設定できます。
# .rspec_parallel --format progress --format ParallelTests::RSpec::FailuresLogger --out tmp/failing_specs.log
これには失敗したテストがログに残ります。標準出力と同じものです。
ここから、再度テストを実行しましょう。
$ grep rspec tmp/failing_specs.log | awk '{print $2}' | xargs bundle exec rspec /Users/osada/projects/mugenup/workstation/vendor/bundle/gems/rspec-core-2.14.7/lib/rspec/core/configuration.rb:896:in `load': cannot load such file -- /Users/osada/projects/mugenup/workstation/spec/views/projects/edit.html.erb_spec.rb:34 (LoadError)
あら、失敗してしまいました。なぜでしょうか?
実際にファイルを開いてみたところ、ascii color code
が含まれていました。
たしかに、これでは、rspec
が通りませんね。
といっても--color
を消してしまうのも、嬉しくありません。
そこで、正規表現を使って、抜き出すことにしました。
- rspec から始まり、「ドット、スラッシュ、コロン、英数字とアンダースコア」で構成される文字列を抽出する
$ ruby -ne 'puts $1 if /(?<=rspec )([\.\/:\w]+)/' tmp/failing_specs.log | xargs bundle exec rspec
これで、失敗したテストのみ、再実行することができます。
rake task
にするべきか?とも思ったのですが、さらに実行を遅らせてしまいそうなので、
alias
の登録だけにしておきました。
こうゆうとき、どういう技を使えばいいか、わからないの
ruby
でワンライナーを書くのは、少し大変ですね。
RuntimeLogger
を使って、テストのグループを平均化する
さて、paralell_tests
はデフォルトでは、ファイルサイズ順に均等化します。
ファイルサイズの大きい順に並べて、4つのグループに順番に追加していくわけです。
RuntimeLogger
を使うことで、テストにかかった時間で、均等化することができます。
--format progress --format ParallelTests::RSpec::RuntimeLogger --out tmp/parallel_runtime_rspec.log
tmp/parallel_runtime_rspec.log
には、テストファイル名と、その実行時間が記録されています。
[feature/parallel stash]~/projects/mugenup/workstation: tail tmp/parallel_runtime_rspec.log …… ./spec/views/sub_projects/show.html.erb_spec.rb:2.261982 ./spec/views/projects/show.html.erb_spec.rb:2.806851
テストのグループ分けのとき、with_runtime_info
メソッドが呼ばれます。
runtime_log
つまりtmp/parallel_runtime_rspec
があるとき、かつ、
今回のtests
ファイル群が、このログの中に含まれていると考えられるときに、
このログを使ってグループ分けします。
puts "Using recorded test runtime"
が出力されれば、成功です。
def with_runtime_info(tests) lines = File.read(runtime_log).split("\n") rescue [] # use recorded test runtime if we got enough data if lines.size * 1.5 > tests.size puts "Using recorded test runtime" times = Hash.new(1) lines.each do |line| test, time = line.split(":") next unless test and time times[File.expand_path(test)] = time.to_f end tests.sort.map{|test| [test, times[File.expand_path(test)]] } else # use file sizes tests.sort.map{|test| [test, File.stat(test).size] } end end
実際に、使用したときと、使用していないときを比較してみます。
[feature/parallel stash]~/projects/mugenup/workstation: RAILS_ENV=test bundle exec bin/rake parallel:spec[model] Finished in 2 minutes 19.6 seconds 78 examples, 0 failures Finished in 3 minutes 19.1 seconds 124 examples, 0 failures Finished in 3 minutes 10.7 seconds 116 examples, 0 failures Finished in 3 minutes 13.1 seconds 126 examples, 0 failures
使用していないときの処理時間は2:19.6 〜 3:19.1
であり、60
秒以上のズレがあります。
一方、runtime_log
を使用したときは下記です。
Using recorded test runtime
が表示されていることが確認できます。
[feature/parallel stash]~/projects/mugenup/workstation: RAILS_ENV=test bundle exec bin/rake parallel:spec[model] Using recorded test runtime Finished in 3 minutes 7.8 seconds 102 examples, 0 failures Finished in 2 minutes 58.3 seconds 108 examples, 0 failures Finished in 3 minutes 10.2 seconds 123 examples, 0 failures Finished in 3 minutes 11.8 seconds 111 examples, 0 failures
2:58.3 〜 3:11.8
であり、13
秒ほどのズレでした。
残念ながら、こちらも大きなスピードアップは感じられませんでしたが、 速くなったよ、という方はご一報いただけると嬉しいです。
まとめ
parallel_tests
とspring
の使い方、いかがだったでしょうか?
- PARALLEL_TESTS_EXECUTABLE="bundle exec rspec" を設定して、
paralell_tests
にspring
を使わせないbin/rspec
を書き換えて、始めのプロセスだけspring
を使うFailuresLogger
を使って、失敗したテストを再実行するRuntimeLogger
を使って、テストのグループを平均化する
弊社でもテストが遅いということが、問題になっており、日々改善に勤しんでおります。
こんなやり方あるよ!という情報をお持ちの方、いらっしゃいましたら、 ご教授いただけますと幸いです。
よろしくお願いします!
宣伝
MUGENUP では、rails を使いたいエンジニアを募集中です。 無限流開発、ご一緒しませんか?
大きな裁量で自社サービス開発!Rubyエンジニアをウォンテッド! - 株式会社MUGENUPの求人 - Wantedly
【探検】Railsカラム更新のメソッド1
初めに
皆さん、初めまして。そして、明けましておめでとうございます。株式会社MUGENUP 開発部の奥田です。 今回、初めて技術ブログを書くことになりました。よろしくお願い致します。
何回かに分けてRailsのカラム更新メソッドについて書いていき、今回はupdate_attribute
とupdate_attributes
について記述します。2つのメソッドは私の環境だとactiverecord-3.2.16/lib/active_record/persistence.rb
にありました。以下の表は簡単なまとめです。
メソッド名 | validation | callback | 更新カラム数 |
---|---|---|---|
update_attribute |
なし | あり | 1つだけ |
update_attributes |
あり | あり | 複数 |
それではそれぞれのメソッドのソースコードを探検してみましょう〜!!
ActiveRecord::Persistence#update_attribute
# Updates a single attribute and saves the record. # This is especially useful for boolean flags on existing records. Also note that # # * Validation is skipped. # * Callbacks are invoked. # * updated_at/updated_on column is updated if that column is available. # * Updates all the attributes that are dirty in this object. # def update_attribute(name, value) name = name.to_s raise ActiveRecordError, "#{name} is marked as readonly" if self.class.readonly_attributes.include?(name) send("#{name}=", value) save(:validate => false) end
save(:validate => false)
となっているので、validationなしということがわかります。Callbackを起こすは英語でinvokeと表現するのですね〜。updated_at
またはupdated_on
は更新するとわざわざ記述しているので、更新しないメソッドもあるのかもしれません。驚いたのはsend("#{name}=", value)
の部分で、sendメソッドはカラム名=
をメソッドと認識することです。sendメソッドを深堀りするのも面白そうだと思いました。
ActiveRecord::Persistence#update_attributes
# Updates the attributes of the model from the passed-in hash and saves the # record, all wrapped in a transaction. If the object is invalid, the saving # will fail and false will be returned. # # When updating model attributes, mass-assignment security protection is respected. # If no +:as+ option is supplied then the +:default+ role will be used. # If you want to bypass the protection given by +attr_protected+ and # +attr_accessible+ then you can do so using the +:without_protection+ option. # def update_attributes(attributes, options = {}) # The following transaction covers any possible database side-effects of the # attributes assignment. For example, setting the IDs of a child collection. with_transaction_returning_status do self.assign_attributes(attributes, options) save end end
update_attributes
にはoptions
があるんですね〜。:without_protection
とoptions
に入れることでmass-assignmentのprotectionチェックなしにできることを初めて知りました。assign_attributes
の中も見たい気持ちはありますが、今後のネタとして取っておきます(笑)。コメントアウトで強調したいところを+
で囲っていますが、私の知らない記法でした。Markdown記法ではないようなので、要調査です。
番外編 ActiveRecord::Persistence#update_attributes!
#update_attributes
はsave
を呼んでいるのでカラム更新の失敗時に戻り値としてfalse
を返します。そして、#update_attribute
に!
をつけた#update_attributes!
の場合はカラム更新の失敗時に例外を返しますが、その中身はどうなっているのでしょう?save
が呼ばれて、!
がついてるということは・・・?
# Updates its receiver just like +update_attributes+ but calls <tt>save!</tt> instead # of +save+, so an exception is raised if the record is invalid. def update_attributes!(attributes, options = {}) # The following transaction covers any possible database side-effects of the # attributes assignment. For example, setting the IDs of a child collection. with_transaction_returning_status do self.assign_attributes(attributes, options) save! end end
予想通りですが、save!
が呼ばれていました〜。
最後に
簡単にではありましたが、Railsのカラム更新メソッドについて見てきました。この記事を読んで初めて知ったことがあれば幸いです。他にもカラム更新のメソッドはあるので、私の次回の記事でもカラム更新のメソッドを探検したいと思います。
宣伝
MUGENUP では、Rails を使いたいエンジニアを募集中です。 無限流開発、ご一緒しませんか?
大きな裁量で自社サービス開発!Rubyエンジニアをウォンテッド! - 株式会社MUGENUPの求人 - Wantedly
【拡張】論理和できるenumを書いてみた【gem】
新年あけましておめでとうございます! MUGENUP の osada です。 2014 年は挑戦の年、ということで、MUGENUP でも新しい事業を初めています。
みんなで作るゲーマー向けの攻略サイト「みなゲー」。 その攻略記事を書いてくださる方を募集しています。 スマホゲームなら俺に任せろ!という豪の方、攻略記事を書いてみませんか?
開発部でも、新しい挑戦としてRuby 2.1
、Rails 4.1
で開発を初めています!
新機能 enum
Rails 4.1 に enum という新機能が実装されました。
Ruby on Rails 4.1 Release Notes — Ruby on Rails Guides
class Conversation < ActiveRecord::Base enum status: [ :active, :archived ] end conversation.archived! conversation.active? # => false conversation.status # => "archived" Conversation.archived # => Relation for all archived Conversations
status など、「特定のグループの中のどれか1つの値を持つ要素」を便利に書けるようになりました。
enum では プレイングマネージャー を表現できない
enum はグループの間でどれか一つの要素を表現します。各要素は排他関係です。:active
と :archived
が同時に存在することはありません。
そう聞くと、グループの複数の値が共存して欲しい。 そう思うことはないでしょうか?
例えば、野球で、監督兼選手は、プレイングマネージャーと呼ばれるそうです。
class User < ActiveRecord::Base enum role: [ :manager, :player ] end
しかしこのとき、enum ではプレイングマネージャーを表現できません。User
がなれるのは、manager(監督)か、player(選手)しかないからです。
論理和できる列挙型、bitwise_enum を作ってみた
bitwise_enum/lib/bitwise_enum.rb at master · osdakira/bitwise_enum
とうことで複数の要素を表現できる、bitwise_enum
を作ってみました。
通常のenum
は、要素を[0, 1, 2]
と表現していくため、
manager
は 0
、palyer
には 1
が入ります。
一方、bitwise_enum
では、manager
は1
、player
には2
が入ります。
プレイングマネージャーを表現したいときは3
にすればOK、ということですね。
これはビット(2進数)に直すと、1桁目がmanager
、2桁目がplayer
を表現していることになります。
このようにすることで、複数の要素を同時に表現することが可能です。
使い方
class User < ActiveRecord::Base bitwise_enum role: [ :manager, :player ] end
基本的な使い方はenum
と同様です。
user = User.new user.manager! user.manager? # => true user.role # => "['manager']" user = User.new user.player! user.player? # => true user.role # => "['player']"
違いとしては、要素が共存できるということです。
user.manager! user.manager? # => true user.player! user.player? # => true user.role # => "['manager', 'player']"
プレイングマネージャーが表現出来ました!
一方で、今までは要素は上書きしていましたが、
bitwise_enum
では、明示的に削除する必要があります。
そのためにnot_xx()
というメソッドがあります。
user.manager! user.manager? # => true user.not_manager! user.manager? # => false
また、全てをリセットするためのreset_xx()
というメソッドがあります。
user.manager! # => ['manager'] user.reset_role # => nil user.role = []
enum
の実装も同じなのですが、manager!
を呼んだ時点で、
update!
が動いているため、即座にSQLが動いてしまいます。
このままでは error ハンドリングするときに困るので、代入するときにもビット演算を行うようになっています。
user.role = :manager user.role # => ['manager'] user.role = :admin user.role # => ['manager', 'admin']
scope も、通常の値ではなく、ビット演算の値でSQLを発行します。
User.manager # => SELECT `users`.* FROM `users` WHERE (role & 1 = 1)
ただコレに関しでは実装部分が、arel
を使わない未熟なものです。
klass.scope value, -> { klass.where("#{name} & #{bit} = #{bit}") }
どなたかarel
の使い方について、情報を教えていただけないでしょうか!
以上が、プレイングマネージャーが実装できるbitwise_enum
の説明です。
gem にしてみた
せっかくなので gem にしてみました。
gem を作るのは簡単でしたが、テスト環境の構築に迷いました。rails に載せずに環境を作るのは難しいですね。
まとめ
- 列挙した要素の値を、重複して持つことができる
bitwise_enum
を実装しました。 - enum のコードにビット演算を追加して実現しています。
皆さんはenum
をどう使われているでしょうか?是非使い方を教えて下さい!
宣伝
MUGENUP では、rails を使いたいエンジニアを募集中です。 無限流開発、ご一緒しませんか?
大きな裁量で自社サービス開発!Rubyエンジニアをウォンテッド! - 株式会社MUGENUPの求人 - Wantedly
【悲報】ActiveSupport::Concern の ClassMethod はモジュールメソッドになる件
皆さんこんにちは!「太鼓式マッサージ?面白そう!」と思ったら「タイ古式マッサージ」でした。MUGENUP の osada です。名前って難しいですよね。
ところで、ActiveSupport::Concern
の ClassMethod
で定義したクラス変数が、どこにあるか、ご存知ですか?
# app/models/concern/item_module.rb module ItemModule extend ActiveSupport::Concern module ClassMethods def my_module_method @@concern_class_variable ||= "concern_class_variable" end end end
# app/models/item.rb class Item < ActiveRecord::Base include ItemModule end
# spec/models/item_spec.rb describe Item do it "ClassMethods で定義すると参照できる" do expect(Item.my_module_method).to eq "concern_class_variable" end end
ここまではOKだと思います。 では、これを自身のクラス変数として参照できないことをご存知でしょうか?
it "ClassMethods で取り込んだクラスメソッドを実行しても自身のクラス変数としては定義されない" do expect{ Item.my_module_method }.not_to change{Item.class_variable_defined?(:@@concern_class_variable)}.from(false) end
参照はできるのに、定義されていない。それは一体どこにあるの?というのが今回のお話です。
クラス変数はどこに定義されているの?
ruby の場合、クラスメソッドはクラスに特異メソッドを追加する、という文脈で定義されます。
Ruby におけるクラスメソッドとはクラスの特異メソッドのことです。 したがって、何らかの方法でクラスオブジェクトにメソッドを定義すれば、そ れがクラスメソッドとなります。 http://docs.ruby-lang.org/ja/2.0.0/doc/spec=2fdef.html#class_method
よって、クラスメソッド内で定義されたクラス変数は、自身のクラス変数となります。
class Item < ActiveRecord::Base include ItemModule class << self def my_class_methods @@my_class_variable = "my_class_variable" end end end
it "クラスメソッドを実行するとクラス変数が定義される" do expect{ Item.my_class_methods }.to change{Item.class_variable_defined?(:@@my_class_variable)}.from(false).to(true) end
これは全く期待通りの動きです。先ほどと違うのは、自身のクラスメソッドとして定義しているという点ですね。 では、class定義ならOKで、module だからダメなんでしょうか?
module の included として定義してみる
ActiveSupport::Concern
では included
というメソッドで、クラス定義の中でコードを評価することができます。
module ItemModule extend ActiveSupport::Concern included do class << self def my_singleton_module_method @@singleton_include_module_method = "singleton_include_module_method" end end end end
この結果はクラス変数はクラスに紐付きました。期待通りです。
it "included で 拡張した特異クラスのクラスメソッドはクラス変数が定義される" do expect{ Item.my_singleton_module_method }.to change{Item.class_variable_defined?(:@@singleton_include_module_method)}.from(false).to(true) end
どうやら、module だからではないようですね。違いは、included
とClassMethod
なので、期待通りに動かないのはClassMethod
のせいのようです。
ClassMethod
とincluded
の違いはどこにあるのか?
ClassMethod
はどのように動くのでしょうか?
base.extend const_get("ClassMethods") if const_defined?("ClassMethods")
ClassMethod
という定義があったら、base
に対してextend
するという実装になっています。
一方includes
は、
base.class_eval(&@_included_block) if instance_variable_defined?("@_included_block")
def included(base = nil, &block) if base.nil? @_included_block = block else super end end
base クラスのclass_eval
を呼び出しているため、自身のコンテキストで評価されます。
違いはClassMethod
はモジュールのextend
であり、included
はクラス定義内での評価というところです。
引数で指定したモジュールのインスタンスメソッドを self の特異 メソッドとして追加します。 instance method Object#extend
extend されたクラス変数はモジュール変数となる
extend
はモジュールのメソッドを、特異クラスのメソッドとして追加するだけです。よって、extend
で追加されたクラスメソッド内で定義されたクラス変数は、モジュール変数になってしまうのです!!!
it "ClassMethods で定義したクラスメソッドは、モジュール変数" do expect{ Item.my_module_method }.to change{ItemModule::ClassMethods.class_variable_defined?(:@@concern_class_variable)}.from(false).to(true) end
そしてさらに恐ろしいことに、モジュール変数はinclude
した全てで共有されます。マニュアルにしっかり書かれています。
モジュールで定義されたクラス変数(モジュール変数)は、そのモジュールをイ ンクルードしたクラス間でも共有されます。 http://docs.ruby-lang.org/ja/2.0.0/doc/spec=2fvariables.html#class
module Foo @@foo = 1 end class Bar include Foo p @@foo += 1 # => 2 end class Baz include Foo p @@foo += 1 # => 3 end
ClassMethod はモジュールメソッドだ
名前って難しいですよね。ClassMethod
だと思っていたら(そして実際にクラスメソッドなのですが)、その中で定義したクラス変数は、モジュール変数になってしまうのです。
これからはできるだけ ClassMethod
は使わないようにしよう、と心に決めた日でした。
[注意] モジュール変数、クラス変数、クラスメソッドの役割について
モジュール変数を定義するべきではない、というのはよく言われていることだと思います。そもそもモジュールは数学的な意味で関数であるべきだというのが私の持論です。副作用がないと言い換えても良いです。
なので、モジュール内でクラス変数を使うべきではないというご指摘は最もだと思います。クラス変数を使わない方が良いというのも納得できます。
本記事は、クラス変数やクラスメソッドの使用を推奨したものではありません。あくまで、知っておくと罠が回避できるかも、という記事ですので、ご了承のほどよろしくお願い致します。
参考
» ActiveSupport::Concern でハッピーなモジュールライフを送る TECHSCORE BLOG
宣伝
MUGENUP では、rails を使いたいエンジニアを募集中です。 無限流開発、ご一緒しませんか?
大きな裁量で自社サービス開発!Rubyエンジニアをウォンテッド! - 株式会社MUGENUPの求人 - Wantedly
シンボルでも文字列でもアクセス可能なHashを使おう!ActiveSupport {Hash編}
皆さんこんにちは!遅れてきた最年長ルーキー MUGENUP の osada です。
さて皆さんは初めてオープンクラスという概念を知った時、驚かれませんでしたか?私はとても驚きました!だって、基本クラスさえもオーバライド可能なんですよ?
[コラム]オープンクラスとは?
同名でクラス定義を行うと、クラスの再定義(上書き)ではなく、クラスへの追加拡張になる仕組み。
class A def a "a" end end a = A.new raise unless a.a == "a" raise if a.respond_to?(:b) # b というメソッドはない # 同名のクラス定義は拡張になる class A def b "b" end end raise unless a.b == "b" raise unless a.respond_to?(:b) # b というメソッドがある # a というメソッドも維持される raise unless a.respond_to?(:a) raise unless a.a == "a"
このオープンクラス、String
など基本クラスさえも拡張可能なのが凄いです。Javascript の prototype みたいですよね。
ActiveSupport コア拡張
オープンクラスという特徴的な仕組みのおかげで、Rails では基本クラスなのに便利なメソッドが沢山ある、という状態になっています。それがActiveSupportのコア拡張です。
ということで、今日のお題はActiveSupport
のHash
です。ターゲットはRails初心者の方。一緒に勉強できれば嬉しいです。
Hash 編
hash拡張のソースは、ここにありました。9ファイル、19メソッドあります。
ruby/2.0.0/gems/activesupport-3.2.16/lib/active_support/core_ext/hash
conversion.rb
まずは Hash 変換するconversion.rb
です。といっても対象はxml。そういえばそんなのもありましたね!
{"foo" => 1, "bar" => 2}.to_xml
できそうな気がしてましたよね?
Hash.from_xml(<<"EOS" <?xml version="1.0" encoding="UTF-8"?> <hash> <foo type="integer">1</foo> <bar type="integer">2</bar> </hash> EOS)
できそうな気がしてましたよね?
deep_dup.rb
ruby の dup
, clone
はシャローコピーだというのはよく知られています。
シャローコピー?という方はRubyist Magazine - 値渡しと参照渡しの違いを理解するがオススメ
deep_dup は深いコピーをしてくれます。当然再帰ですよね〜。
def deep_dup duplicate = self.dup duplicate.each_pair do |k,v| tv = duplicate[k] duplicate[k] = tv.is_a?(Hash) && v.is_a?(Hash) ? tv.deep_dup : v end duplicate end
ところで、duplicate[k]
とv
の値が異なることがあるのでしょうか?
deep_merge.rb
深いコピーがあるなら、深いマージもあるでしょう。そうでしょう。
こちらで興味深いのは、破壊メソッドを作っておいて、それを dup
と組み合わせれば通常のメソッドになるということです。センスありますね。
def deep_merge(other_hash) dup.deep_merge!(other_hash) end # Same as +deep_merge+, but modifies +self+. def deep_merge!(other_hash) other_hash.each_pair do |k,v| tv = self[k] self[k] = tv.is_a?(Hash) && v.is_a?(Hash) ? tv.deep_merge(v) : v end self end
diff.rb
Hash の差分って、どうされてますか? Array なら マイナス[1,2] - [2,3] # => [1]
が使えますが、Hash にはありません。ここでdiff
の出番です
しかしこのdiff
、 差分ではなく、差異なんです。
[6] pry(main)> { 1 => 2 }.diff({1 => 2, 2 => 2, 3 => 2}) => {2=>2, 3=>2}
2つのハッシュの相違点を取り出すメソッドなんですね。実装はとてもシンプルで、ハッシュ1とハッシュ2に同じkey
を見つけてvalue
も同じときは消す。ハッシュ2からハッシュ1にあるkeyを消す。そしてマージです。
def diff(h2) dup.delete_if { |k, v| h2[k] == v }.merge!(h2.dup.delete_if { |k, v| has_key?(k) }) end
なんと悲しいことに、Rails4 では Deprecated になっていました!
def diff(other) ActiveSupport::Deprecation.warn "Hash#diff is no longer used inside of Rails, and is being deprecated with no replacement. If you're using it to compare hashes for the purpose of testing, please use MiniTest's assert_equal instead." dup. delete_if { |k, v| other[k] == v }. merge!(other.dup.delete_if { |k, v| has_key?(k) }) end
Hash#deep_diff
, the recursive difference between two hashes by nikitug · Pull Request #8142 · rails/rails
deep_diff
を使えばいいじゃん、ということですが、まだ時期ではない、ということで、deprecated
を付けるだけになっているようです。しばらくは、diff
ということでしょうか。
with_indifferent_access.rb
params がシンボルでも文字列でもアクセスできて、不思議だなぁ?と思われたことありませんか? 普通は nil が返ってきます。
[8] pry(main)> h = {a: 1, "b" => 2} => {:a=>1, "b"=>2} [9] pry(main)> h[:a] => 1 [10] pry(main)> h["a"] => nil [11] pry(main)> h[:b] => nil [12] pry(main)> h["b"] => 2
そんな時に使うのか、with_indifferent_accessです!
[13] pry(main)> h = {a: 1, "b" => 2}.with_indifferent_access => {"a"=>1, "b"=>2} [14] pry(main)> h[:a] => 1 [15] pry(main)> h["a"] => 1 [16] pry(main)> h[:b] => 2 [17] pry(main)> h["b"] => 2
このメソッドは、HashWithIndifferentAccess
へのプロキシメソッドです。
def with_indifferent_access ActiveSupport::HashWithIndifferentAccess.new_from_hash_copying_default(self) end
それが何かと言いますと、
module ActiveSupport class HashWithIndifferentAccess < Hash
Hash のサブクラスでした。そして、new_from_hash_copying_default(self)
は
def self.new_from_hash_copying_default(hash) new(hash).tap do |new_hash| new_hash.default = hash.default end end
となっていますので、やっていることは、元 hash の default を自身に適用しているだけです。まさに継承している感じですね。
ではコンストラクタに秘密があるのでしょうか?
def initialize(constructor = {}) if constructor.is_a?(Hash) super() update(constructor) else super(constructor) end end
この場合、super
はHash
クラスの呼び出しですから、ミソはupdate
にあるようです。あれ?update
ってマージするメソッドだったような?
def update(other_hash) if other_hash.is_a? HashWithIndifferentAccess super(other_hash) else other_hash.each_pair { |key, value| regular_writer(convert_key(key), convert_value(value)) } self end end
ハッシュが既にHashWithIndifferentAccess
だった場合は、そのままインスタンス化するだけです。するとelse
が心臓部のようです。
other_hash.each_pair { |key, value| regular_writer(convert_key(key), convert_value(value)) }
文字列でもシンボルでも
key 側の処理はこんな感じ。
def convert_key(key) key.kind_of?(Symbol) ? key.to_s : key end
単に Symbol だったら文字列に変換しているんですね。ずるい!なんてシンプルな実装なんでしょうか。
このconvert_key
を各所で使うため、key は全てシンボルではなく文字列になります。
でしたよね?
[13] pry(main)> h = {a: 1, "b" => 2}.with_indifferent_access => {"a"=>1, "b"=>2}
convert_value
value側の処理です。
def convert_value(value) if value.is_a? Hash value.nested_under_indifferent_access elsif value.is_a?(Array) value.dup.replace(value.map { |e| convert_value(e) }) else value end end
1 . value が Hash だったときは、indifferent_access に変換して返します。 というか単にエリアスが貼ってあるだけでした。
alias nested_under_indifferent_access with_indifferent_access
普通に
value.with_indifferent_access
と書いても良い所をあえてvalue.nested_under_indifferent_access
と書く所、こだわっていますね。
2 . value がArray のとき、
elsif value.is_a?(Array) value.dup.replace(value.map { |e| convert_value(e) })
再帰して返すというお決まりのコードです。ところでなぜ
value.map { |e| convert_value(e) }
ではいけないのでしょうか?
これはおそらく、is_a?
は親クラスであっても true になるからだと思います。
value が Array のサブクラスだった場合、`value.map { |e| convert_value(e) }
では、サブクラスではなく、Arrayが返ってしまいますからね。
ということで、心臓部はこの2つのメソッドで、後は全て、conver_*
を経由するように書き換えているだけでした。例えば、key?
は convert_key
を経由するように書き換えているだけです。
def key?(key) super(convert_key(key)) end
シンボルでも文字列でもアクセスできる、というのは key
と value
に対して、根本のところで文字列に変換してしまう、という方法で実装されていました。
これさえ理解しおけば、シンボルでも文字列でも扱えるHashが使いこなせますね!
except.rb
Hash の中から、特定の key だけを消したいこと、結構ありますよね?これは有名なので皆さんご存知かと思います。
[12] pry(main)> {a: 1, b: 2}.except(:a, :c) => {:b=>2}
def except(*keys) dup.except!(*keys) end # Replaces the hash without the given keys. def except!(*keys) keys.each { |key| delete(key) } self end
keys.rb
Hash の key に対するメソッド群です。
- stringfy_keys
def stringify_keys dup.stringify_keys! end # Destructively convert all keys to strings. Same as # +stringify_keys+, but modifies +self+. def stringify_keys! keys.each do |key| self[key.to_s] = delete(key) end self end
delete で取り出しながら、key を変換して格納し直すという、 エレガントな入れ替えですね。
- 逆にシンボル化もできるようです。
def symbolize_keys dup.symbolize_keys! end # Destructively convert all keys to symbols, as long as they respond # to +to_sym+. Same as +symbolize_keys+, but modifies +self+. def symbolize_keys! keys.each do |key| self[(key.to_sym rescue key) || key] = delete(key) end self end
(key.to_sym rescue key)
とか、シンプルでかっこいいですね~。
そういえば、to_options とかご存知でした?
alias_method :to_options, :symbolize_keys alias_method :to_options!, :symbolize_keys!
- assert_valid_keys で key の確認を便利にできます。
def assert_valid_keys(*valid_keys) valid_keys.flatten! each_key do |k| raise(ArgumentError, "Unknown key: #{k}") unless valid_keys.include?(k) end end
reverse_merge.rb
渡した方の hash にマージするメソッドです。
self.reverse_merge(other_hash)
ではなく
other_hash.merge(self)
でいいんじゃないの?
ところが、面白い箇所が1点!
merge!( other_hash ){|key,left,right| left }
って何ですか?
def reverse_merge(other_hash) other_hash.merge(self) end # Destructive +reverse_merge+. def reverse_merge!(other_hash) # right wins if there is no left merge!( other_hash ){|key,left,right| left } end alias_method :reverse_update, :reverse_merge!
同じkeyがあったとき、左側(self)を優先にできるようです。
[24] pry(main)> {a: 1, b: 2}.reverse_merge!({a: 1, c: 3}) => {:a=>1, :b=>2, :c=>3} [25] pry(main)> {a: 1, b: 2}.reverse_merge({a: 1, c: 3}) => {:a=>1, :c=>3, :b=>2} [26] pry(main)> {a: 1, b: 2}.merge({a: 1, c: 3}) => {:a=>1, :b=>2, :c=>3} [27] pry(main)> {a: 1, b: 2}.merge!({a: 1, c: 3}) => {:a=>1, :b=>2, :c=>3}
破壊型のときは、自分に取り込むよ! ということで自分が優先なんでしょうか? merge に block が渡せるのは、初めて知りました。勉強になります
slice.rb
特定のkey だけを取り出したいこと、よくありますよね?
[15] pry(main)> a = {a: 1, b:2} => {:a=>1, :b=>2} [16] pry(main)> a.slice(:a, :c) => {:a=>1} [17] pry(main)> a => {:a=>1, :b=>2}
def slice(*keys) keys = keys.map! { |key| convert_key(key) } if respond_to?(:convert_key, true) hash = self.class.new keys.each { |k| hash[k] = self[k] if has_key?(k) } hash end
特定のkeysだけ取り出して、それ以外は返り値で返すとか、delete っぽくでオシャレ!Hashの分割で使えますね。
[18] pry(main)> a.slice!(:a, :c) => {:b=>2} [19] pry(main)> a => {:a=>1}
def slice!(*keys) keys = keys.map! { |key| convert_key(key) } if respond_to?(:convert_key, true) omit = slice(*self.keys - keys) hash = slice(*keys) replace(hash) omit end
逆に特定のkeys だけを返してくれるメソッドもありました。
[20] pry(main)> a = {a: 1, b:2} => {:a=>1, :b=>2} [21] pry(main)> a.extract!(:a, :c) => {:a=>1} [22] pry(main)> a => {:b=>2}
def extract!(*keys) result = {} keys.each {|key| result[key] = delete(key) } result end
まとめ
ということで、何か新しい発見はあったでしょうか?
9ファイル19メソッドのHash拡張でした。
with_indifferent_access, deep_dup, except, slice, slice!
辺りは、どんどん使っていきたいと思います。
便利なメソッドがあったら、是非教えて下さい!
宣伝
MUGENUP では、rails を使いたいエンジニアを募集中です。 無限流開発、ご一緒しませんか?
大きな裁量で自社サービス開発!Rubyエンジニアをウォンテッド! - 株式会社MUGENUPの求人 - Wantedly
最速で!最短で!真っ直ぐに!Facebookグループの書き込みを監視する方法!
皆さん、こんにちは!WAというと Web Application ではなく、RPGの方を思い出す MUGENUP の osada です。
いきなりの個人的な話で恐縮ですが、私が初めて書いたWeb Application はPHP
でした。当時 Web というと、「SSI で include」したり、「perl で cgi」とか、少し大掛かりだと「Java Applet」?、程度しか知らなかった私には、HTMLの中にプログラムが書けるというのは衝撃的なことでした。PHPの真髄は、すぐに動くことにあるのではないかと思っています。そしてさらに個人的な意見で恐縮ですが、ソースコードで最も価値があるのは動くコードだと思っています。少し学術的に言えば、合目的性ですね。そう言う意味で、PHPは私の好きな言語の一つです。PHP、良い言語ですよね!
はい、ということで今回は、コードの美しさとか全くなし。動けばOK、ということで、Facebook Group への書き込みをウォッチして、新着があったらメールで知らせる ツールを作ってみたいと思います。
え、理由ですか?単に必要だったからです(笑)。
Facebook Developer に登録して、アプリを作成
facebook developer へ登録
app へ
新しいアプリを作成 ボタン
新しいアプリを作成
-
承認
こんな感じに
Graph APIエクスプローラーを使用する
アクセストークンを取得
user groups にチェック
friends groups にチェックして、[Get Access Token]
アクセストークンをコピー
Facebook にアクセスする
koala
と メール送信用にmail
をインストール
# Gemfile source 'https://rubygems.org' gem "koala", "~> 1.8.0rc1" gem "mail" group :test, :development do gem "pry" gem 'pry-byebug' end
$ bundle
14.書く
watch_facebook.rb
# -*- coding: utf-8 -*- require "koala" require "mail" ACCESS_TOKEN = ARGV[0] GROUP_ID = ARGV[1] USER_NAME = ARGV[2] PASSWORD = ARGV[3] EMAILS = ARGV[4..-1] SMTP_SETTINGS = { address: 'smtp.gmail.com', port: 587, domain: 'smtp.gmail.com', user_name: USER_NAME, password: PASSWORD } FROM = "hoge-report@mugenup.com" SUBJECT = 'Hoge Report' begin load "config.rb" rescue LoadError end puts "access facebook ..." graph = Koala::Facebook::API.new(ACCESS_TOKEN) feeds = graph.get_connections(GROUP_ID, "feed") puts "reject already feeds ..." already_feeds_file = open("already_feeds.txt", "a+") already_read_ids = already_feeds_file.readlines.map(&:chomp) new_feeds = feeds.reject{|f| already_read_ids.include?(f["id"]) } exit if new_feeds.size == 0 puts "create messages ..." messages = new_feeds.map do |f| <<-EOS #{f["from"]["name"]} #{f["message"][0..100]}... #{'-' * 50} EOS end messages << "https://www.facebook.com/groups/#{GROUP_ID}/" puts "send mail new feeds ..." mail = Mail.new do from FROM to EMAILS.join(",") subject SUBJECT body messages.join("\n") end mail.charset = 'utf-8' mail.delivery_method :smtp, SMTP_SETTINGS mail.deliver! puts "store feed_ids (#{new_feeds.size}) ..." already_feeds_file.puts(new_feeds.map{|f| f["id"]}) already_feeds_file.close
15.使う
$ bundle exec ruby watch_facebook.rb ACCESS_TOKEN GROUP_ID USER_NAME PASSWORD EMAILS
← 適宜読み替えて入力してください。
または、config.rb
というのを作って、
こんな感じに書いてから、
$ bundle exec ruby watch_facebook.rb
16.ACCESS_TOKENの長期化
このままだと、ACCESS_TOKEN の有効期間は1時間です。これを長期化します。
omniauth を入れる(心の)余裕は無いんです!
ということで、下記を参考に、(7)で確認した、app-idとapp-secret、および、アクセストークンを入力して、送信します。
https://graph.facebook.com/oauth/access_token? grant_type=fb_exchange_token& client_id={app-id}&client_secret={app-secret}&fb_exchange_token={short-lived-token}
Unable to get a long term access token using facebook graph api - Stack Overflow
17.長期化されたアクセストークンを使う
こんな感じで、新しく長期化されたアクセストークンが取得できます。
expires
が 5184000
になっていることが確認できます。約2ヶ月、使い続けられます。
18.後は cron で登録するなりなんなり!
あ、cron
でbundle
付きを呼ぶのはちょっと面倒ですが、頑張ってください!
ということで、Facebookのグループを監視するアプリが作れました!
ちょっと解説
設定を引数から受け取る部分
ACCESS_TOKEN = ARGV[0] GROUP_ID = ARGV[1] USER_NAME = ARGV[2] PASSWORD = ARGV[3] EMAILS = ARGV[4..-1] SMTP_SETTINGS = { address: 'smtp.gmail.com', port: 587, domain: 'smtp.gmail.com', user_name: USER_NAME, password: PASSWORD } FROM = "hoge-report@mugenup.com" SUBJECT = 'Hoge Report'
初めに、一番速いのはなんだろう?と思って、ACCESS_TOKEN
を直書きしたのですが、これをコミットしてはいけないとゴーストが囁いたので、引数で渡すことにしました。
設定をファイルから受け取る部分
begin load "config.rb" rescue LoadError end
そうこうしているうちに、毎回引数で渡すのが面倒くさい、ということになり、ファイルに格納することにしました。このconfig.rb
は .gitignore
で対象外にしました。
Facebook のグループからメッセージを獲得する部分
puts "access facebook ..." graph = Koala::Facebook::API.new(ACCESS_TOKEN) feeds = graph.get_connections(GROUP_ID, "feed")
特に解説することはありません
既に確認済みのメッセージを削除する部分
puts "reject already feeds ..." already_feeds_file = open("already_feeds.txt", "a+") already_read_ids = already_feeds_file.readlines.map(&:chomp) new_feeds = feeds.reject{|f| already_read_ids.include?(f["id"]) } exit if new_feeds.size == 0
……特に解説することはありません
メール送信用のメッセージを作る部分
puts "create messages ..." messages = new_feeds.map do |f| <<-EOS #{f["from"]["name"]} #{f["message"][0..100]}... #{'-' * 50} EOS end messages << "https://www.facebook.com/groups/#{GROUP_ID}/"
......……特に解説することはありません(まさか解説する箇所は出てこないのでしょうか?)
メールを送信する部分
puts "send mail new feeds ..." mail = Mail.new do from FROM to EMAILS.join(",") subject SUBJECT body messages.join("\n") end mail.charset = 'utf-8' mail.delivery_method :smtp, SMTP_SETTINGS mail.deliver!
当初mail
コマンドを使用していたのですが、
connect to aspmx3.googlemail.com[74.125.137.26]:25: Connection refused
ということで、mac
のsmtp
からダイレクトに送れなかったため、mail
gemを導入しました。
これがなければ、もっとシンプルでもっと速く作れたのに、と思うとちょっと残念です。
さて、Mail ですが、送信相手が複数居る時は、カンマで区切りましょう。
charaset
を utf-8
にしないと、Non US-ASCII detected and no charset defined.
という警告がでます。
(説明することが合って良かったぁ〜)
送信したメッセージのIDを記録しておく部分
puts "store feed_ids (#{new_feeds.size}) ..." already_feeds_file.puts(new_feeds.map{|f| f["id"]}) already_feeds_file.close
特に説明することはありません!
まとめ
以上が、Facebookの特定のグループの新着をメールで送信するツールです。
全くシンプルに、ただ動くことだけを考えて書いたコードですが、いかがだったでしょうか?
ついつい、「DBを使おう」とか「ActiveSuppoだけでも使おう」とか、「関数に分けよう」とか、「クラスを作ろう」とか色々考えてしまうのですが、問題を解決することだけに注目するというのも、また楽しいプログラミングですよね!
そうそうPHPが好きだと言いましたが、単に作成者のRasmus Lerdorf
氏が好きなだけかもしれません。彼の発言は一読の価値があると思いますので、宜しければ一度、Rasmus Lerdorf - Google 検索してみてください!
宣伝
MUGENUP では、rails を使いたいエンジニアを募集中です。 無限流開発、ご一緒しませんか?
大きな裁量で自社サービス開発!Rubyエンジニアをウォンテッド! - 株式会社MUGENUPの求人 - Wantedly
あ、WAでビンタしあう方を思い出す方は友達になりましょう。