実例アプリで学ぶ“Railsらしさ”の基礎:Railsで目指せ、情熱エンジニア(6)(1/2 ページ)
Ruby on Railsで書かれた実例アプリを取り上げて、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」テンプレートを使うなどなかなか意欲的です。
このアプリケーションの作りですが、以下のような機能があります。
- 認証(devise)
- ユーザーが自分の記事のURLをフォームに記入
- 自分の記事がブックマークの一覧として表示されるとともに、記事のタイトル、retweetの数、hatenaブックマークの数などをウェブから取ってきてくれてます
自分の記事の評価というのは常々気になるものなので、かゆいところに手が届くサービスといって良いでしょう。単にデータベースを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
ああ、なるほど。ここに、アプリの全てのロジックが詰まっているようですね。
ItemsControllerを分析してみる
これからいろいろとItemのコントローラを中心にダメ出しをしていきますが、その前に良い点についても触れておきたいと思います。
(1)エラー処理に対応している
最初に西村さんは「とにかく動くものを作って公開」とおっしゃっていました。そのような態度で望んだ場合、往々にして全てが思った通りに動くことを前提にしてしまい、その他のことがおろそかになってしまいがちです。しかしながら、以下のラインではURLがちゃんとしたものか確認しています。
if @item.url !~ /^(#{URI::regexp(%w(http https))})$/ then
そして、以下のラインでは、外部のHTMLを取りに行った際に返事が返ってこないときのことも見越して、タイムアウト時の挙動もちゃんと設定しています。
rescue Timeout::Error
(2)アクセス権限をしっかり設定している
先ほど述べたエラー処理にも通じますが、アスセス権限の対応も初めてのプロジェクトでは見過ごしてしまいがちですが、ここではbefore_filterを用いて編集、使用としているアイテムのオーナーユーザーとログインしているユーザーが同一かどうか確認しています。
before_filter :authorise_as_owner
(3)メソッドを分かりやすい形で細かく分けている
このメソッドは良い例と悪い例が混在しているのですが、今は良い点のみ注目します。
def populate(item) populate_title(item) populate_hatena(item) populate_retweet(item) end
上のメソッドは自分自身では何もせず、記事のタイトル、hatenaブックマーク数、retweet数を検出する各メソッドを呼び出しているだけです。このようにメソッドに分かりやすい名前を付けてあると、各メソッドの実装を見なくてもだいたいの概要はつかめるので、コードを後から読む人に対して(それは数カ月後の自分かもしれませんが)親切だと思います。
Copyright © ITmedia, Inc. All Rights Reserved.