概要
原著者の許諾を得て翻訳・公開いたします。
- 英語記事: 7 Gems Which Will Make Your Rails Code Look Awesome
- 公開日: 2017/10/14
- 著者: Val Zavadskiy
- サイト: https://blog.rubyroidlabs.com/
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
があり、たとえばChargeCard
やSendThankYou
が失敗すれば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を使うのは、よりエレガントに目的を達成できるからです。
次の素晴らしい記事も合わせてご覧ください。
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-types、dry-struct、dry-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で皆様が快適な開発生活を送ることを願っています。質の高いコードを高い表現力で書けるツールを他にもご存知でしたら、ぜひ私どもまでお知らせください。