Ruby on Railsで書かれた実例アプリを取り上げて、Rails初心者が陥りがちなコードの書き方を指摘します。より「Railsらしい」コーディングを目指そう!
今回からRailsで書かれた実際のWebアプリの例で、リファクタリングとテストについて解説します。取り上げるのは「Worklista」です。
Worklistaは、@IT編集部の西村賢さんによる作品です。deliciousやhatenaブックマークのような一種のブックマークサービスですが、特徴は自分の記事を1カ所にまとめることに特化していることです。私の場合、個人のブログより会社のブログ、あるいは今回の記事のように商業サイトに書いたりと、自分の作品が散在しているので、このようなまとめサイトがあると非常に便利です。
ちなみに、作者である西村さんに、作成の経緯と、連載中でリファクタリングの実例に使ってもよいかと聞いたところ、以下のように言っています。
このWebアプリ、地域RubyコミュニティのAsakusa.rbで、プロの皆さんに見て頂いて、ボコボコに言われてみたいなと思っていたところなんです。Asakusa.rb創始者の松田さんにも、Worklistaで何かしゃべりませんか、という風に言っていただいていて。ちなみに自己弁護的にいうと、Arelをちゃんと使いたいとか、そういえばDBのインデックスどうするのかなとか、そもそも全部renderじゃなくてパーシャルにしろよとか、そういうのは何となく分かっているのですけど、とにかく動くものを作って公開するのを最優先しています。特にパフォーマンスは今はまったく問題外と思っています。
これまで、何かWebサービスを作ってみたいとか、実際PHPで途中まで作った経験はあるんです。でも、結局出せませんでした。それで、「もう言い訳はやめて、今度こそは出す」とちょっとゴリゴリっとやったんです。
「もう言い訳はやめて、今度こそは出す」というのは非常に良い心がけだと思います。そしてパフォーマンスなども、最初は気にしないというのも良い姿勢です。サービスのボトルネックというのは実際にユーザーに使ってもらうと意外なところから出て来たりするものです。そういうのを最初から憶測し、それにそってアーキテクチャを構築したりすると「YAGNI」と言われてしまいます。
YAGNIというのは「You ain't gonna need it」、「たぶん必要にならないと思うよ」ぐらいの意味です。
リファクタリングを始める前に、まずはコードレビューを通して改善点のヒントを探って行きましょう。以下がapp以下の主なファイルの配置です。なお、今回解説の対象としているソースコードは、GitHub上の、ここから、またリファクタリング後のものは、ここから、たどることができます。今回は関係ありませんが、開発中の最新版は、ここからたどれます。
|-- controllers
| |-- items_controller.rb
| |-- pages_controller.rb
| `-- users_controller.rb
|-- helpers
|-- models
| |-- item.rb
| |-- tag.rb
| |-- tagging.rb
| `-- user.rb
`-- views
|-- devise
|-- items
| `-- edit.html.haml
|-- pages
| |-- about.html.haml
| `-- home.html.haml
`-- users
|-- index.html.haml
|-- me.html.haml
`-- show.html.haml
認証のプラグインとして最近人気を博している「Devise」、“マークアップ俳句”を標榜するHTML生成のための「Haml」テンプレートを使うなどなかなか意欲的です。
このアプリケーションの作りですが、以下のような機能があります。
自分の記事の評価というのは常々気になるものなので、かゆいところに手が届くサービスといって良いでしょう。単にデータベースをCRUD(Creat、Read、Update、Delete)する機能はRailsのActiveRecordが色々と用意してくれていますが、外部のWebからHTMLを取ってきて、そこからタイトルを抽出するといった機能はRuby力を磨く良いチャンスです。
第2回でお話しした、私の作った初めてのRailsアプリであるcommect.usも、外部のフォームからコメントを抽出する機能がありました。初めてのプロジェクトをする上でうってつけなアプリだと思います。
modelsにはuser、tag、tagging、itemの4つのモデルがあります。itemがこのアプリケーションの要となるもので、ここに各ユーザーのブックマーク情報が格納されています。
では、このアプリの肝であるitem.rbを覗いてみましょう。
class Item < ActiveRecord::Base
belongs_to :user
has_many :taggings, :dependent => :destroy
has_many :tags, :through => :taggings
# let us do the url validation in the contorller
attr_writer :tag_names
after_save :assign_tags
def tag_names
@tag_names || tags.map(&:name).join(' ')
end
private
def assign_tags
if @tag_names
self.tags = @tag_names.split(/\s+/).map do |name|
Tag.find_or_create_by_name(name)
end
end
end
end
あれ、思ったほどコードがないですね?
主なロジックと言えば、ブックマークをセーブする時に、それと関連したタグも作成するといったところでしょうか。外部からHTMLを取ってくるロジックはどこでしょう? そしてこのコメントが少し気になります。
# let us do the url validation in the contorller
では、コントローラも覗いてみましょう。
require 'open-uri'
require 'nkf'
require 'timeout'
require 'resolv-replace'
class ItemsController < ApplicationController
before_filter :authorise_as_owner
conf = APP_CONFIG["bitly"]
@@bitly = Bitly.new(conf["username"], conf["apikey"])
def create
@user = User.find(params[:user_id])
@item = @user.items.new(params[:item])
if @item.url !~ /^(#{URI::regexp(%w(http https))})$/ then
flash[:notice] = "Invalid URL!!"
redirect_to user_recent_path(current_user.username)
return
end
begin
Timeout::timeout(8){
@doc = open(@item.url).read
}
rescue Timeout::Error
flash[:notice] = "Timeout! Could not retrieve data from the URL!!"
redirect_to user_recent_path(current_user.username)
return
end
guess_date @item
populate @item
if @item.save
flash[:notice] = "Created an item. Any changes?"
redirect_to edit_user_item_path(current_user, @item)
else
render :action => 'new'
end
end
def destroy
@item = Item.find(params[:id])
@item.destroy
flash[:notice] = "Successfully destroyed an item."
redirect_to user_recent_path(current_user.username)
end
def edit
@item = Item.find(params[:id])
end
def update
@item = Item.find(params[:id])
populate_hatena @item
populate_retweet @item
if @item.update_attributes(params[:item])
flash[:notice] = "Successfully updated item."
redirect_to user_recent_path(current_user.username)
else
render :action => 'edit'
end
end
private
def authorise_as_owner
@user = User.find(params[:user_id])
unless (user_signed_in? && @user == current_user)
# You are not the owner of this item!
flash[:notice] = "Oops, something went wrong!"
redirect_to users_path
end
end
def guess_date(item)
if @doc =~ /(20\d{2}\/[01]?\d\/[012]?\d)/ then
date = Date.strptime($1, "%Y/%m/%d")
end
if date then
item.published_at = date
else
item.published_at = Time.now
end
end
def populate(item)
populate_title(item)
populate_hatena(item)
populate_retweet(item)
end
def populate_title(item)
item.title = item.url
@doc.match(/<title>([^<]+)<\/title>/) do |m|
if m.size == 2 then
title = m[1]
item.title = NKF.nkf("--utf8", title)
end
end
end
def populate_hatena(item)
hatena_api = "http://api.b.st-hatena.com/entry.count?url="
url = item.url
num = open(hatena_api+url).read
num = 0 if num == ""
item.hatena = num
end
def populate_retweet(item)
url = @@bitly.shorten(item.url)
item.bitly_url = url.short_url
item.retweet = url.global_clicks
end
end
ああ、なるほど。ここに、アプリの全てのロジックが詰まっているようですね。
これからいろいろとItemのコントローラを中心にダメ出しをしていきますが、その前に良い点についても触れておきたいと思います。
最初に西村さんは「とにかく動くものを作って公開」とおっしゃっていました。そのような態度で望んだ場合、往々にして全てが思った通りに動くことを前提にしてしまい、その他のことがおろそかになってしまいがちです。しかしながら、以下のラインではURLがちゃんとしたものか確認しています。
if @item.url !~ /^(#{URI::regexp(%w(http https))})$/ then
そして、以下のラインでは、外部のHTMLを取りに行った際に返事が返ってこないときのことも見越して、タイムアウト時の挙動もちゃんと設定しています。
rescue Timeout::Error
先ほど述べたエラー処理にも通じますが、アスセス権限の対応も初めてのプロジェクトでは見過ごしてしまいがちですが、ここではbefore_filterを用いて編集、使用としているアイテムのオーナーユーザーとログインしているユーザーが同一かどうか確認しています。
before_filter :authorise_as_owner
このメソッドは良い例と悪い例が混在しているのですが、今は良い点のみ注目します。
def populate(item) populate_title(item) populate_hatena(item) populate_retweet(item) end
上のメソッドは自分自身では何もせず、記事のタイトル、hatenaブックマーク数、retweet数を検出する各メソッドを呼び出しているだけです。このようにメソッドに分かりやすい名前を付けてあると、各メソッドの実装を見なくてもだいたいの概要はつかめるので、コードを後から読む人に対して(それは数カ月後の自分かもしれませんが)親切だと思います。
Copyright © ITmedia, Inc. All Rights Reserved.