連載
» 2011年05月26日 12時00分 公開

Railsで目指せ、情熱エンジニア(6):実例アプリで学ぶ“Railsらしさ”の基礎 (1/2)

Ruby on Railsで書かれた実例アプリを取り上げて、Rails初心者が陥りがちなコードの書き方を指摘します。より「Railsらしい」コーディングを目指そう!

[井上真,New Bamboo]

実際の例でRailsらしさを知る

 今回からRailsで書かれた実際のWebアプリの例で、リファクタリングとテストについて解説します。取り上げるのは「Worklista」です。

 Worklistaは、@IT編集部の西村賢さんによる作品です。deliciousやhatenaブックマークのような一種のブックマークサービスですが、特徴は自分の記事を1カ所にまとめることに特化していることです。私の場合、個人のブログより会社のブログ、あるいは今回の記事のように商業サイトに書いたりと、自分の作品が散在しているので、このようなまとめサイトがあると非常に便利です。

@IT編集部の西村賢さんがRuby on Railsで開発した「Worklista」

 ちなみに、作者である西村さんに、作成の経緯と、連載中でリファクタリングの実例に使ってもよいかと聞いたところ、以下のように言っています。

 この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数を検出する各メソッドを呼び出しているだけです。このようにメソッドに分かりやすい名前を付けてあると、各メソッドの実装を見なくてもだいたいの概要はつかめるので、コードを後から読む人に対して(それは数カ月後の自分かもしれませんが)親切だと思います。

       1|2 次のページへ

Copyright © ITmedia, Inc. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。