TDDBC2.0に参加してきた
TDDBC2.0(札幌)に参加してきた。僕にとっては初めてのTDDBCだったので正直ついて行けるかかなり心配な中での参加だった。
演習ではRubyを選択し、@ayuminさん、@sandinistさんと組ませていただいた。お二人ともRubyはもちろん vim の使い方や Github にも慣れていて一緒にペアプロしているだけで非常に勉強になった。TDDの形式は一定時間たったらドライバとプログラマが交代するという乱取り形式というやり方。最初はタイマーで5分を計りながらやっていたけれどちょっと時間が少な過ぎるということで大体10分、もしくは区切りのいいところまでという感じですすめた。
気づいたこと、感想など
- バージョン管理の徹底。演習始まってすぐに@ayuminさんがすかさず GitHubにリポジトリを立てて共有してくれた。コードと向き合う前にバージョン管理環境を整えるという見本のような行動だった。早速それぞれのマシンに clone し、演習中は push と pull を繰り返しながら自分がプログラマでない時もコードを見ながらアイデアを出し合ったりした。細かい区切りで「ではコミットしましょうか」と促してくれたので、GitHubのコミットログを見るだけでどのような歴史で演習に取り組んでいたのかが一目瞭然だった。さらにいつの間にかコミットした瞬間にコミットログが Twitterにつぶやかれるようにしてくれていたのにも驚いた。
- 仕様化テスト。rspec を使ってレガシーコードの仕様化テストを書き始めて、テストを書きながら仕様を理解していくというのを実感できた。仕様を理解できればリファクタリングのアイデアも浮かんで来る。テストがあるから既にレガシーコードではなくリファクタリングも恐くなくなった。バージョン管理も徹底されているから壊れたらいつでも戻れるという安心感がよりリファクタリングを後押しする。
- レガシーコードとの戦い方。まずはファイル分割し、多少手を加えてでもテストしやすいように慎重にI/Fを見直す。テストコードが書き難い所にはそもそも構造的な問題がありそうで、その辺りをリファクタリング対象としてチェックしていく。ある程度テストでセーフティーネットができたというところでリファクタリング。様子を見ながら不安なところにテストを追加していく。カバレッジもリアルタイムで監視しながらやれると仕様化テストの網羅率も明確にできてもっと良かったかなと思った。
- 同時に戦わない。作業を進めて行くと、バグフィックス、テストコードのリファクタリング、テストの追加、コードのリファクタリングなど今すぐやりたいことが同時に複数現れ始めるけれど、そこは一旦落ち着いて今やるべきことをメンバで整理/認識合わせして進めることが大事だと思った。「ここは一旦テストを綺麗にしてから、次のテストを書きましょう」とか「リファクタリングして構造を綺麗にしてからバグフィックスするためのI/Fを考えましょう」など。一度に複数の敵を相手にしてはいけない。あと、ついついコードを触りすぎてしまいそうになる時も「ここはまだテストコードが無いから触っちゃだめだね」と正しい手順を意識しながら取り組むことで不可解なバグやデグレに遭遇することも少なかった気がする。
- 良いテストコード。今回は外部ファイルに書き出すプログラムだったので、テストコードで吐き出すファイルと本物コードが吐き出すファイルとが混ざって多少混乱した。テストは何度でも実行可能でなければならなく依存関係を無くす、という原則に乗っ取って、テストコード用の出力ファイルを分けることや後始末をするタイミングが重要だと感じた。
- テストの自働化。これは@ayuminさんが演習の終盤で環境を作っていて、guard-rspecを使ってファイルを保存した瞬間にテストが自動的に流れて結果が growl に表示されるという究極的にすばやいフィードバックを得られるようにしていた。自分もそれを見習って帰ってから早速環境構築してみた。コードやテストを編集しながら自分の想定通りに結果が自動的に通知してくれるのを横目で確認しつつ、次々とタスクに取りかかれるのはものすごく快適。想定外の結果の時だけちゃんとテスト結果を確認すれば良いので思考が中断されにくいのがいい。時にはコンソールを切り替えてテストを実行し、結果に対してみんなで一喜一憂するのも大事なアクティビティだと思うけれど、一人で黙々と作る時は作業効率アップへの強い武器になると感じた。
- 道具にこだわる。お二人とも vim をかなり使いこなしていてものすごいスピードでコードを書いたりファイル間を移動したりしていた。超基本動作しか使っていない自分とは大違いだった。特にタイマーで時間を計りながら作業していたから5分で自分が書けるコードの量の少なさを実感できたし、みなさんに申し訳なく思った。自分が使う道具の使い方にこだわって、より効率の良い使い方を追求することの重要性を知った。
- 3脚椅子。演習は言ってしまえばものすごく小さいソフトウェア開発なんだけれど、その中でさえもソフトウェア開発の三本柱(三脚椅子)の、バージョン管理、テスティング、自働化を徹底することの重要性を実感できた。僕の知る「何百キロステップ作ってます!」というような大規模開発を行っているプロジェクトでもこれらを徹底しているところはほとんど無いんじゃないだろうか。それってプロの仕事じゃない。すぐに腐ってしまうレガシーコードを大量生産しているだけだ。
まとめ
いろいろ書いたけれど、ソフトに対して真摯に向き合っている人達と一緒に開発できた今回の経験は自分に取ってとても大きな宝になった。今後のテストやコードへの向き合い方の大きなヒントをたくさんいただいた。最近は開発現場から離れてしまって直接活かせる機会は減ってしまったけれど、少しずつでも現場の人達に伝えていけるといいな。
このような貴重な機会を作っていただいた @shuji_w6e さんやスタッフのみなさま、素晴らしい講演や演習フォローをしていただいた@t_wadaさん、本当にありがとうございました。
- 演習で作ったソース
RubyのCGIプログラムがWindowsで激重な件の対応
症状
Linux上でサクサク動いていた Ruby の CGI プログラムを Windows に移植するとレスポンスがめちゃくちゃ遅くなってしまった。いろいろ調べると require 時のI/O処理が重いらしい。
I/O自体を高速化する方法などを調べたがヒットしなかった。さすがに重すぎて実用に耐えられないのでどうしようか悩む。。。
対応策
dRubyを使って処理を分散させる方法を見つけた。
要するにボトルネックとなる require を含む重い処理をまとめてマシン立ち上げ時に一発実行し、アクセスの度に起きる cgi では dRuby で立ち上げておいたリモートオブジェクトにリクエストを渡して処理するという方法。
リモートオブジェクトは戻り値としてレスポンスの文字列を返すようにし、受け取った cgi はそれを print する。 cgi では 'drb/drb' と 'cgi' くらいしか require しなくて済むため高速化が期待できる。
具体例
■todo_srv.rb #!C:/ruby/bin/ruby.exe require 'drb/drb' # こいつらが重い処理 require "rubygems" require "active_record" require "uri" require "kconv" class Todo def do_request(input) mode = input['mode'].first # レスポンス作成処理 case mode : return res end end def main() # dRuby でリモートオブジェクトを生成して寝て待つ uri = "druby://localhost:12345" DRb.start_service(uri, Todo.new) puts DRb.uri sleep end main()
■todo.cgi #!C:/ruby/bin/ruby.exe require "drb/drb" require "cgi" input = CGI.new # 作っておいたリモートオブジェクトを取得 todo = DRbObject.new_with_uri('druby://localhost:12345') print "Content-type: text/html\n\n"; # リクエストを渡し、結果を print することでレスポンスを返す print todo.do_request(input.params)
上記のように元々1ファイルの cgi で頑張っていた処理を2ファイル構成にし、todo_srv.rb をあらかじめマシン起動時に実行しておくことで、ボトルネックとなっていた重いrequire の処理群を初回の一度のみに集約できる。そして todo.cgi ではリモートオブジェクトから結果をもらうだけなのでレスポンスが劇的に速くなった。(約10秒掛かっていたレスポンスが1秒未満に!)
dRuby は初めて使ってみたけれど、かなり使えるなぁ。
Quick JUnit を使う
slim3でテストを書いてみようとしたけど、JUnit の実行が面倒なのでショートカットで簡単に実行できる方法が無いか探してみた。Quick JUnitというプラグインがあるとのことなので早速試してみる。
Quick JUnit のインストール
- 以下のサイトを参考に最新バージョンのURLを取得「最新版(0.6.0):http://quick-junit.sourceforge.jp/updates/current/」
- [Help]->[Install New Software...] を選択
- [Work with:]の欄に上記URLを記入して[Add]する
- 後は流れに任せてインストールする
簡単な使い方
参考:https://github.com/kompiro/quick-junit/tree/master/ja/
- 「テスティングペアを開く」
- 「JUnitを実行する」
- Ctrl + 0 (Ctrl 押しながら数字キーの 0 を押す) : 開いているテストコードを実行する。テストを書いたそばから瞬時に実行できる。サクサク実行できてものすごく便利。
- 「JUnitをデバックモードで実行する」
- Ctrl + Shift + 0 (Ctrl と Shift を押しながら数字キーの 0 を押す) : 開いているテストコードをデバックモードで実行する。
とりあえず上記のショートカットを覚えるだけでもかなり使える。ありがとう、まさーるさん!
メール機能を実装してみる
GAEで作成してみている「かんばんりすと」の内容をメールで自分の携帯に送ってみたくなった。どうしてWebアプリなのにわざわざこんなことがしたいのかというと、自分は携帯のパケット定額プランには入っていなくて(節約のため au の Eプラン)、リストをチェックするにもパケット料金が掛かって嫌だなぁと思い、テキストの軽いページを作って凌いでいたが、twitterにメールでアクセスできる「やぶみん」というサービスを使っていて「なんでもメールで送っちゃえば無料じゃん、これはいい!」、と思ったからである。
で、GoogleAppEngineのアプリ内からメールを送信する方法は既にいろんなところで書かれていたのでやってみた。特に新しい話題は無いので以下は完全に自分用のメモ。
仕様イメージ
実装
- jspの適当な位置に以下を記述。サブミットでsendMailAjax()メソッドにメアドを設定して呼ぶ。
<form method="post" onsubmit="sendMailAjax($('#mail_addr').get(0).value);return false;"> <input id="sendmail_button" type="submit" value="SendMail" style="font-size:12px" /> <input type="text" id="mail_addr" name="ttl" value="" size="40" style="font-weight:normal;padding:2px;background:#ffc"/></br> </form>
- javascript で Ajax を使ってイベント送信
function sendMailAjax(addr) { $('#info').html("Sending mail to " + addr + " ..."); $.ajax({ type: "POST", cache: false, url: "sendmail", data: "mail_addr=" + addr, success: function(response){ $('#info').html(response); } }); }
- Ajaxから飛んで来る sendmail に対応したコントローラ SendmailController を生成する
- とりあえずベタっと書いてみた
- try{} の中がメール送信のメイン部分。
- 送信元アドレスはアプリの開発者のメアドでないといけなかったため、別途 kanban.list@gmail.com のアカウントを新規で取得し、このアプリの開発者に追加した。(GAEアプリ設定画面の[Administration]->[Permissions]から登録)
- エラーがスローされなかった場合はメアドをクッキー登録している(setMailAddrToCookie)
- タスク表示のページでメアドのクッキーの内容を次回表示するようにする。(コードは省略)
- レスポンスは OK か NG かの文字列を Ajax のハンドラに投げる
- 複数の送信先を指定したい場合は msg.addRecipient() を何度も呼べば良い
package slim3.controller.todo; import org.slim3.controller.Controller; import org.slim3.controller.Navigation; import java.io.UnsupportedEncodingException; import java.util.Properties; import javax.mail.MessagingException; import javax.mail.Message; import javax.mail.Session; import javax.mail.Transport; import javax.mail.internet.InternetAddress; import javax.mail.internet.MimeMessage; import javax.servlet.http.Cookie; import slim3.service.todo.TodoTaskService; public class SendmailController extends Controller { TodoTaskService service = new TodoTaskService(); @Override public Navigation run() throws Exception { String user = sessionScope("user"); String mail_addr = requestScope("mail_addr"); String title_text = user + " りすと"; // DataStoreからメール送信したいテキストを取得 String list_text = service.getSendMailText(user); //メールを送る try { Session session = Session.getDefaultInstance(new Properties(), null); MimeMessage msg = new MimeMessage(session); msg.setFrom(new InternetAddress("kanban.list@gmail.com", "かんばんりすと", "ISO-2022-JP")); msg.addRecipient(Message.RecipientType.TO, new InternetAddress(mail_addr)); //送信先のメールアドレス msg.setSubject(title_text, "ISO-2022-JP"); msg.setText(list_text, "ISO-2022-JP"); Transport.send(msg); setMailAddrToCookie(mail_addr); response.getWriter().write("<font color=blue>SendMail OK! to " + mail_addr + "</font>"); } catch (UnsupportedEncodingException e) { response.getWriter().write("<font color=red>SendMail NG-EncordError! to " + mail_addr + "</font>"); } catch (MessagingException e) { response.getWriter().write("<font color=red>SendMail NG-MessageingError! to " + mail_addr + "</font>"); } return null; } protected void setMailAddrToCookie(String addr){ int cookie_age_sec = 60*60*24*30*12; // 12 month Cookie cookie = new Cookie("mail_addr",addr); cookie.setMaxAge(cookie_age_sec); cookie.setPath("/"); response.addCookie(cookie); } }
- 携帯で受け取るには当たり前だけどメールフィルターの設定で kanban.list@gmail.com を通るようにする。(しばらく気がつかずハマった)
- ローカルの環境だと上手く動いていたとしてもメールは飛んでこないみたい。デプロイして確認する。
- メール送信処理は別メソッドかサービスに分けたいけど、どういうI/Fにすればよいのか悩む。
- メール受信の方はまだ未着手。以下を参考にやってみる
- やっぱりメールで受け取れると凄く便利。家とかでタスクに登録して、出がけにメール送信しておいて後で見られる。自分が実際に使っている用途としては、日用品で足りないものをリストに日々 Waiting に登録しておき、買い物に行く際にメールで送信しておいて出先で見ながら買い物するという感じ(だから Waiting だけわかればいい)。今までは直にアプリにアクセスしていたらパケット代もったいなかったんだよなぁ。まさに自分用の節約機能。
ビジネスロジックをどこに書くか
Rails をかじっていくと、主に view と controller がごちゃごちゃしてくる。
あまり考えずに作っていくと以下のような状態になっちゃった
- view: 様々な action からの遷移で表示したいものを :action で判定したりするコードが増える
- controller: model から様々な形式の検索方法でデータを取得してきてそれなりの配列とかに加工
- model: あまり処理なし。ほとんど空
自分の実感としてはMVCで考えると以下のようなイメージ
- view: レイアウトがわかるようなレベルで処理を書きすぎない。デザインは css なのかな。
- controller: view と model の仲介だから model から取得した値をあれこれいじりすぎずに view に渡す。逆に view からもらったデータを画面遷移とかに必要な最小限のデータだけ見てあとは model に渡す
- model: DBから取得する値に対するあらゆる加工を施すメソッドを持つ。controller からは必要最小限のメソッド呼び出しで必要なデータを取得できるようにする
ユニットテストの観点からも view とか controller のテストって結構面倒だと思っているので、なるべく複雑そうなところは model に押しこんでテストしやすくするというのもあると思う。
世間はどうかなと調べてみたら以下のようなサイトがあった。
大体方向性は一緒かな。どの層でもそれなりに書いて動かせちゃうからみんな悩むんだろうな。
MVCの役割をしっかり考えてコードを書かないといけない。で、たぶん共通処理など曖昧な役割の処理をどこに書くかでまた悩むんだろう。
今の認識は以下のような感じ。
- view: 部分テンプレート、ヘルパ
- controller: ヘルパ?
- model: ヘルパ?
そういえばDBに直結しない model というのもアリなんだろうか。それとか複数の model を束ねる modelとか。
この辺の実現方法も今後必要になりそうだから抑えておきたい。
それと、twitter で悩みをつぶやいていたら @snoozer05 さんから以下の参考サイトを教えていただいた。
これはわかりやすい。最初 view に全てのコードを書いてぐちゃぐちゃになっている状態から徐々に controller と model に正しい責務で処理を分割していく過程を説明してくれている。
この考え方をお手本にして今後取り組んでみよう。
lambda でクロージャを使ってみる
いくつかの関数を配列にまとめて入れてまとめて実行したくなったので初めて lambda を使ってみた。
- func1 と func2 を funcs 配列に入れて each でまとめて呼びたいケース
def func1( arg ) lambda { # arg を使っていろいろ if なんちゃらエラーチェック return #エラーの場合に return で処理を中断する end } end def func2( arg ) lambda { # arg を使っていろいろ } end def main a_arg = "hoge" funcs = [] funcs << func1( a_arg ) funcs << func2( a_arg ) funcs.each {| a_func | a_func.call } end main
なるほど、lambda をリターンするメソッドを作れば関数オブジェクトとして使えるしクロージャ(この場合は arg がクロージャ対象かな)としても使える。また、lambdaでは lambda 内のエラー処理で return で処理を打ち切れたけど、一見同じように使える Proc.new では「unexpected return (LocalJumpError)」というエラーになってしまった。break でも同様。ちょっとこの辺りの挙動の違いはまだよくわかっていない。
- return したらエラーになる例
def func3( arg ) Proc.new { # arg を使っていろいろ if なんちゃらエラーチェック return #エラーの場合に return で処理を中断する end } end
lambda とかブロックとか Proc とか調べ始めると面白い。&の使い方など。
以下とか参考になる。今後のコードで効果的に使っていきたい。
- sasata299's blog: Rubyで "&" を使うと幸せになれるらしいよ
- http://blog.livedoor.jp/sasata299/archives/51382454.html
- ソースコード備忘録Press: Ruby Proc.newとlambdaの違い
- http://yukirin.dontexist.org/archives/145
rspec で rr を使ってみた
インストール
# gem install rr
初期設定
- spec ファイルの先頭に以下を追加
RSpec.configure do |config| config.mock_with :rr end
mock の使い方
- 最後の {} は戻り値
#テスト対象。Dir.glob の引数を評価したい。同時に戻り値も [] で返したい Dir.glob("../src/exist_ver") #mockコード mock(Dir).glob("../src/exist_ver") {[]}
- 2011/04/13追記: 複数回呼び出しを期待する場合
- まったく同じ呼び出され方を複数回された場合はエラーになってしまうので、例えば2回呼び出されることを期待する場合は以下のように書く
- .twice を付加する。3回以上はどうなるんだろう・・・。
#mockコード mock(Dir).glob("../src/exist_ver").twice {[]}
stub の使い方
- 戻り値のみ設定する
#stubコード stub(Dir).glob {[]}
mock があれば stub は使わないんじゃ? 引数は厳密に判定したくはないが、戻り値をサクっと返して欲しいとかのケースなのかな。まだ使い分けがよく分かっていない。
あ、もしかして、渡された引数によって戻り値のバリエーションを変えたい場合などに mock を使うのかな。でも、同じ呼ばれ方で1度目と2度目で別の戻り値を返したい場合などはどう書くのだろう・・・。