Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

Railsコードを改善する7つの素敵なGem(翻訳)

概要

原著者の許諾を得て翻訳・公開いたします。

Rubyroid Labsの別記事「Ruby on Railsで使ってうれしい19のgem(翻訳)」も合わせてどうぞ。

Railsコードを改善する7つの素敵なGem(翻訳)

私たちRubyroid Labはアプリのアーキテクチャに多くの情熱を注ぎ込んでいます。手がけているプロジェクトの多くが長期にわたっているので、設計のどこかで少し油断すると、機能を1つ追加するのにプロジェクトをスクラッチからやり直す方が早い、といった事態になりかねません。こんな目には遭いたくないものです。

新しく参加したメンバーがロジック把握のためにソースコードを読みとおすだけでかなり時間がかかるようなら、それはプロジェクトが病んでいることを示す兆候のひとつです。本記事では、コードを整理してチームメンバーの笑顔を取り戻してくれるさまざまなgemをご紹介いたします。

1. interactor固定リンク

何らかの複雑なビジネスロジックを書くときには必ず私たちのリストに入るほど素晴らしいライブラリです。さてinteractorとは何でしょうか?Readmeには「ビジネスロジックのカプセル化というひとつの目的だけを持つシンプルなオブジェクトです」とあります。ビジネスロジックのカプセル化といえば皆さんの大好きなService Objectを連想するかもしれませんが、interactorはずっと機能が豊富です。早速コード例をご覧に入れましょう。

# app/interactors/create_order.rb
class CreateOrder
  include Interactor

  def call
    order = Order.create(order_params)

    if order.persisted?
      context.order = order
    else
      context.fail!
    end
  end

  def rollback
    context.order.destroy
  end
end
# app/interactors/place_order.rb
class PlaceOrder
  include Interactor::Organizer

  organize CreateOrder, ChargeCard, SendThankYou
end

コード例を見れば、このgemの実に素晴らしい機能がいくつもあることにお気づきかと思います。
まず、シンプルなinteractorをいくつかまとめて1つの実行可能なチェインにできるという点です。contextという特殊変数は、異なるinteractor同士でステートを共有するのに使われています。
次に、interactorのひとつが何らかの理由で失敗すると、それまでのinteractorはすべてロールバックします。CreateOrderクラスには#rollbackがあり、たとえばChargeCardSendThankYouが失敗すればorderは破棄されます。
実にクールなgemですね。

2. draper固定リンク

Railsでヘルパーを自作したことがあれば、時間とともにヘルパーが増えて手に負えなくなったことがあるでしょう。こうしたヘルパーは一部のデータの整形表示に使われることがほとんどです。このようなときはDecoratorデザインパターンの出番です。draperの文法は、見ればだいたいわかるようになっています。

# app/controllers/articles_controller.rb
def show
  @article = Article.find(params[:id]).decorate
end
# app/decorators/article_decorator.rb
class ArticleDecorator < Draper::Decorator
  delegate_all

  def publication_status
    if published?
      "Published at #{published_at}"
    else
      "Unpublished"
    end
  end

  def published_at
    object.published_at.strftime("%A, %B %e")
  end
end
<!-- app/views/articles/show.html.erb -->
<%= @article.publication_status %>

上のコードを見ると、published_at属性を特定のフォーマットで表示するのが目的になっています。古典的なRailsウェイでは、こういう場合に2つの選択肢がありました。

1つ目は単にそれ用のヘルパーを書くことです。この場合、すべてのヘルパーが同じ名前空間に属するので、プロジェクトが長期化するに連れていまいましい名前衝突が発生するようになり、デバッグも非常に困難になります。

2つ目はモデルの中にメソッドを書いてそれを使うことですが、モデルのクラスの責務を超えてしまうのでよろしくありません。モデルのデフォルトの責務は「データのやりとり」であり、データの表現方法ではないからです。

こういう場合にdraperを使うのは、よりエレガントに目的を達成できるからです。

次の素晴らしい記事も合わせてご覧ください。

Ruby on Railsで使ってうれしい19のgem(翻訳)

3. virtus固定リンク

シンプルなRubyオブジェクトをそのまま使っても要件を満たせないことがあります。たとえば1つのページに複雑なフォームがいくつもあり、フォームごとに異なるモデルとしてデータベースに保存しなければならないとします。こんなときはvirtusです。次の例をご覧ください。

class User
  include Virtus.model

  attribute :name, String
  attribute :age, Integer
  attribute :birthday, DateTime
end
user = User.new(:name => 'Piotr', :age => 31)
user.attributes # => { :name => "Piotr", :age => 31, :birthday => nil }

user.name # => "Piotr"

user.age = '31' # => 31
user.age.class # => Fixnum

user.birthday = 'November 18th, 1983' # => #<DateTime: 1983-11-18T00:00:00+00:00 (4891313/2,0/1,2299161)>

# mass-assignment
user.attributes = { :name => 'Jane', :age => 21 }
user.name # => "Jane"
user.age  # => 21

見てのとおり、virtusは標準的なOpenStructクラスと似ていますが、ずっと機能が豊富です。ぜひvirtusを隅々まで試してみてください。

virtusは手始めに使うのによいgemですが、もっと高度な技法を使いたい場合は、dry-typesdry-structdry-validation gemもぜひお試しください。

4. cells固定リンク

Nick Suttererの手によるRuby on Railsの高度なアーキテクチャをまだご存じない方は、ぜひ一度チェックしてみましょう。このアーキテクチャのコンセプト全体を既存のアプリに必ずしも適用できるとは限りませんが、ユーザーの種類などに応じた条件をいくつも適用していくうちにビューが複雑になりすぎてしまったらcells gemの出番です。cellsはビューの一部を切り離し、Rubyの通常のクラスとしてコンポーネント化できます。次のコードサンプルをご覧ください。

# app/cells/comment_cell.rb
class CommentCell < Cell::ViewModel
  property :body
  property :author

  def show
    render
  end

  private

  def author_link
    link_to "#{author.email}", author
  end
end
<!-- app/cells/comment/show.html.erb -->
<h3>New Comment</h3>
  <%= body %>

By <%= author_link %>
# app/controllers/dashboard_controller.rb
class DashboardController < ApplicationController
  def index
    @comments = Comment.recent
  end
end
<!-- app/controllers/dashboard/index.html.erb -->
<% @comments.each do |comment| %>
  <%= cell(:comment, comment) %>
<% end %>

上の例では、ダッシュボードに最近のコメントを表示したいと考えています。すべてのコメント表示をアプリ上で同一にすることが前提です。Railsはレンダリングに共有パーシャルを使うこともできますが、代わりにCommentCellオブジェクトを使います。このオブジェクトは、前述のdraperにビューのレンダリング機能を組み合わせたものとみなせますが、もっと機能が豊富です。すべてのオプションの詳細についてはgemのReadmeをご覧ください。

5. retryable固定リンク

現代的なWebアプリにはさまざまな機能が統合されています。確実なAPI呼び出しが使えることもありますが、ファイルのFTPアップロードや何らかのバイナリプロトコルを使わなければならないこともあります。後者の問題は、呼び出しがこれといった理由なしにときどき失敗することです。このような場合にできる最善手はリトライです。次のようなコードを書いて切り抜けなければならなかったことがあるでしょう。

begin
  result = YetAnotherApi::Client.get_info(params)
rescue  YetAnotherApi::Exception => e
  retries ||= 0
  retries += 1
  raise e if retries > 5
  retry
end

こんなときこそretryableの出番です。このgemを使って上のコードを次のように書き直してみましょう。

Retryable.retryable(tries: 5, on: => YetAnotherApi::Exception) do
  result = YetAnotherApi::Client.get_info(params)
end

コードがずっとすっきりしましたね。retryableは他の状況にも使えるので、ぜひチェックしてみてください。

6. decent_exposure固定リンク

(Rubyの)マジックがあまり好きでない方はこのライブラリを使わなくてもよいでしょう。しかしアプリによっては、きわめてシンプルな標準CRUDアクションがたくさん複製されることがあります。こんなときにはdecent_exposureの出番です。何かを管理する新しいコントローラを作成するときを考えてみましょう。scaffoldすると次のような感じになります。

class ThingsController < ApplicationController
  before_action :set_thing, only: [:show, :edit, :update, :destroy]

  def index
    @things = Thing.all
  end

  def show
  end

  def new
    @thing = Thing.new
  end

  def edit
  end

  def create
    @thing = Thing.new(thing_params)

    respond_to do |format|
      if @thing.save
        format.html { redirect_to @thing, notice: 'Thing was successfully created.' }
      else
        format.html { render :new }
      end
    end
  end

  def update
    respond_to do |format|
      if @thing.update(thing_params)
        format.html { redirect_to @thing, notice: 'Thing was successfully updated.' }
      else
        format.html { render :edit }
      end
    end
  end

  def destroy
    @thing.destroy
    respond_to do |format|
      format.html { redirect_to things_url, notice: 'Thing was successfully destroyed.' }
    end
  end

  private

  def set_thing
    @thing = Thing.find(params[:id])
  end

  def thing_params
    params.require(:thing).permit(:for, :bar)
  end
end

コードが60行にもなればもう少ないとは言えません。Rubyistたるものコードはいつもできるだけ最小限に保ちたいものです。decent_exposureを使えば、以下のようなコードを書けます。

class ThingsController < ApplicationController
  expose :things, ->{ Thing.all }
  expose :thing

  def create
    if thing.save
      redirect_to thing_path(thing)
    else
      render :new
    end
  end

  def update
    if thing.update(thing_params)
      redirect_to thing_path(thing)
    else
      render :edit
    end
  end

  def destroy
    thing.destroy
    redirect_to things_path
  end

  private

  def thing_params
    params.require(:thing).permit(:foo, :bar)
  end
end

できました!何ひとつ機能を失わずにコードが30行そこそこにまで減っています。お気付きのとおり、すべてのマジックを発揮しているのはexposeです。内部の詳しい動作を理解するにはgemのドキュメントをご覧ください。

7. groupdate固定リンク

開発者なら誰しも、異なるタイムゾーンを扱うつらさが身に沁みていることでしょう。データベースで集約(aggregation)を行うときは特にそうで、私も「今月のユーザー数を日別で取れるようにせよ: ただし無料ユーザーは除くこと」などといったオーダーはいつも悩みの種です。でもこのgemがあればそんな心配から解放されます。次の例をご覧ください。

User.paid.group_by_week(:created_at, time_zone: "Pacific Time (US & Canada)").count
# {
#   Sun, 06 Mar 2016 => 70,
#   Sun, 13 Mar 2016 => 54,
#   Sun, 20 Mar 2016 => 80
# }

ご紹介した7つのgemで皆様が快適な開発生活を送ることを願っています。質の高いコードを高い表現力で書けるツールを他にもご存知でしたら、ぜひ私どもまでお知らせください。

関連記事

Railsで重要なパターンpart 1: Service Object(翻訳)

Ruby on Railsで使ってうれしい19のgem(翻訳)


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。