Rails -『Crafting Rails 4 Applications』を読んで

はじめに

Crafting Rails 4 Applications – Expert Practives for Everyday Rails Development (Jose Valim)』を読んで学んだ内容を整理する。

Crafting Rails 4 Applications

本書について

概要

近年、テスト駆動開発やDataMapperの利用をはじめ、開発者のアプリ設計ニーズが多様化している。これらのニーズに対応するためにRailsでもplug-in APIを利用すれば、様々なアプリ設計が可能となる。本書は、そのようなplug-in APIの実装例を紹介し、より生産性が高く、処理速度の早いRailsアプリについての理解を与える。

Ruby – 中級者向けの本『Effective Ruby』を読んで に続いて、Railsのベストプラクティスを把握するためにこの本を選んだ。

対象者

中級・上級Rails開発者

目次

  1. Creating Our Own Renderer
  2. Building Models with Active Model
  3. Retrieving View Templates from Custom Stores
  4. Sending Multipart Emails Using Template Handlers
  5. Streaming Server Events to Clients Asynchronously
  6. Writing DRY Controllers with Responders
  7. Managing Application Events with Mountable Engines
  8. Translating Applications Using Key-Value Bank Ends

1. Creating Our Own Renderer

この章で学ぶこと

  • Rails plug-insの基本設計
  • renderメソッドのカスタマイズ方法
  • rendering-stackの基本

実装例

デフォルトのrenderメソッドではPDFフォーマットに対応していないので、Rails pluginを生成してRailsの機能を拡張させる実装例。

ポイント

  • PDFを書き出す部分は、ライブラリPrawnに依拠する
  • rails pluginを作った場合、/test/dummyの中にテスト用のアプリが生成されている
  • ActionController::Renderersにpdfフォーマットを追加する
  • 同じ手順を踏めば、別の拡張子を追加することも可能
  • サンプルコードは以下の通り

module PdfRenderer
  require "prawn"
  ActionController::Renderers.add :pdf do |filename, options|
    pdf = Prawn::Document.new
    pdf.text render_to_string({})
    send_data(pdf.render, filename: "contents.pdf", disposition: "attachment")
  end
end

Rails5でのplugin生成手順

以下のコマンドでプラグイン用のRailsアプリを生成。

$ rails plugin new NAME

Rails5の場合、デフォルトで記述されているgemspec”TODO”を書き換えて、bundle installを実行。

$ bundle install

2. Building Models with Active Model

この章で学ぶこと

  • Active Modelとそのモジュール
  • Railsが求めるActive Model APIにオブジェクトを適用する方法
  • RailsのバリデータとRuby-constant lookup

実装例

Railsのviewとcontrollerで利用可能なActive ModelメーラーのRailsプラグインの実装例。

ポイント

  • autoloadメソッドは、それを記載したコードが実行されたタイミングで該当する定数が読み込まれるので、遅延ロードの方法としてよく用いられている
  • attribute_method_prefixやattribute_method_suffixでオブジェクトのattributeに関連したメソッド名を動的に定義することができる
  • prefixとsuffixを同時に利用したい場合は、attribute_method_affixを使う
  • RailsのActive Model APIに従うことで、controllerやviewからオブジェクトを利用できるようになる
  • Mailerの機能はActionMailer::Baseを継承させて実装
  • DOMを操作してのテストにはcapybaraが一般的に使われている
  • スパム対策に隠しフィールド(honey pot)を用意し、これに値が入力されている時は常にバリデーションエラーとする
  • extend ActionModel::Callbacksを利用してコールバックをカスタマイズ

includeとextendの違い

includeメソッドは、クラスやモジュールに他のモジュールをインクルード(Mix-in)します。引数にはモジュールを指定します(複数指定できます)。戻り値はクラスやモジュール自身です。(Rubyリファレンス)

性質は継承と同じで、取り込むモジュールは対象クラスとスーパークラスの間に特異クラスとして入る。

extendメソッドは、オブジェクトの特異クラスにモジュールを取り込み、モジュールのメソッドを特異メソッドとして使えるようにします。取り込むモジュールは引数moduleに指定します(複数指定できます)。戻り値はレシーバ自身です。(Rubyリファレンス)

一方、extendは取り込むモジュールのメソッドを特異メソッドとして使えるようにする。

sendメソッドについて

sendメソッドは、レシーバの持っているメソッドを呼び出します。第1引数nameにはメソッド名をシンボルか文字列で指定します。メソッドの引数を指定したいときは、第2引数arg以降に引数を並べます。戻り値は、呼び出したメソッドの戻り値が返ります。(Rubyリファレンス)

以下コード例、Rubyリファレンスより引用。


class Cat
  def hello(n = nil)
    n ? Array.new(n, "meow!") : "meow!"
  end
  
  private
  def sleep
    "zzz..."
  end
end
 
cat = Cat.new
p cat.send(:hello) # "meow!"
p cat.send(:hello, 3) # ["meow!", "meow!", "meow!"]
p cat.send(:sleep) # "zzz..."

3. Retrieving View Templates from Custom Stores

この章で学ぶこと

  • DBテンプレートをlook upさせるためのRailsレンダリングスタックのカスタマイズ方法
  • Ruby Hash lookupの挙動
  • ActionController::Metalを使ったコントローラの高速化

実装例

Railsアプリとしてテンプレート管理システムを作る実装例。

ポイント

  • append_view_path を使えば、デフォルトのview pathを変更することができる
  • RailsのResolver APIに従ってさえすれば、view pathはファイルシステムを外部に設定することも可能
  • nested hashは配列ハッシュやハッシュハッシュより読み込み速度が速い(以下参照)

require "benchmark"

nested_hash # => {"foo"=>{"bar"=>true}}
array_hash  # => {["foo", "bar"]=>true}
hash_hash   # => {{:a=>"foo", :b=>"bar"}=>true}

Benchmark.realtime {1000.times {nested_hash[foo][bar]}}
# => 0.0002620000159367919
Benchmark.realtime {1000.times {array_hash[["foo", "bar"]]}}
# => 0.003488999995170161
Benchmark.realtime {1000.times {hash_hash[{a: "foo", b: "bar"}]}}
# => 0.0049189999990630895
  • cache_keyなどを使ったオブジェクトは配列を含むハッシュより読み込み速度が速い(以下参照)

require "benchmark"

cache_key = Object.new
details = {
  formats:  [:html, :xml, :json],
  locale:   [:en],
  handlers: [:erb, :builder, :rjs]
}

hash_1 = { cache_key => 10 }
hash_2 = { details => 10 }

Benchmark.realtime {1000.times {hash_2[details]}}
# => 0.00039099997957237065
Benchmark.realtime {1000.times {array_hash[["foo", "bar"]]}}
# => 0.005313999979989603
  • ApplicationControllerの継承関係は以下の通り。
AbstractController::Base
↓
ActionController::Metal
↓
ActionController::Base
↓
ApplicationController
  • 個々のコントローラでActionControllerMetalを継承させ、必要なモジュールだけインストールすればオーバヘッドリクエストの解消できる(以下、実装例引用)

class CmsController < ActionController::Metal
    include ActionController::Rendering
    include AbstractController::Helpers
    prepend_view_path SqlTemplate::Resolver.instance
    helper CmsHelper

    def respond
        render template: params[:page]
    end
end

module CmsHelper
    # 機能追加はここに書いていく
    #(layoutsの追加など)
end

protectedとprivateの違い

protected methods can be called by any instance of the defining class or its subclasses. (stack overflow)

protectedメソッドは、レシーバがそのクラスかそのサブクラスであれば呼び出し可能。

private methods can be called only from within the calling object. You cannot access another instance's private methods directly. (stack overflow)

privateメソッドは、そのオブジェクト内でしか呼び出しできない。

Singletonとは

Mix-inによりsingletonパターンを提供する。Singletonモジュールをincludeすることにより、クラスは 高々ひとつのインスタンスしか持たないことが保証される。Singletonをmix-inしたクラスの クラスメソッドinstanceはその唯一のインスタンスを返す。newはprivateメソッドに移され、外部から呼び出そうとするとエラーになる。

そのclassのインスタンスの存在が1つだけで良い場合に利用される。

4. Sending Multipart Emails Using Template Handlers

この章で学ぶこと

  • Rails template-handler APIについて
  • 複数テンプレートを使ったActionMailer
  • Rails generators & railties

実装例

ActionMailerには複数のテンプレート(html & txt)が必要。これはメール受信者のメールクライアントがhtmlを読めなかった場合のバックアップとしてtxt形式が求められるため。ここでは、マークダウン記法で書いた1つのテンプレートでActionMailerを使えるようにする。

ポイント

  • RubyのショートカットをQ{}使えば、カッコ内のメソッドを利用できる。
  • Markdownコンパイラライブラリには処理速度が速い "RDIscount" を使う
  • templateをコンパイルするコードは、下記のようにbegin/endで囲み1行で書く。これは、エラーがあった時、その箇所と内容をはっきりさせるため。

RDiscount.new(begin;nil.this_method_does_not_exist!;end).to_html
  • Mailerのmailメソッドはrespond_toメソッドのようにフォーマットオプションを与えることができる。

mail(to: @recipient, from: "test@example.com") do |format|
    format.text
    format.html
end
  • text・htmlの両方の場合において、同じテンプレート(.merb)を参照させ、MERBモジュール内でコンパイル方法を条件分岐させる。
  • Railsのイニシャライズやコンフィグのデフォルトをいじりたい時は、pulginにRails::Railtieをインクルードさせる。

Missing :controller key on routes definition

上記エラーが出た場合は、routes.rbを以下のように変更すればテストが通るようになる。


Rails.application.routes.draw do
  # get "/handlers/:action", to: "handlers"
  get "/handlers/:action", to: "handlers##{:action}"
end

5. Streaming Server Events to Clients Asynchronously

この章で学ぶこと

  • Rails engines
  • Rials live-streaming functionality
  • Ruby threads and queues
  • RailsでのコードのEager load

実装例

スタイルシートに変更があった場合ストリーミングでクライアントに反映させるRailsエンジンの実装例。

ポイント

  • Rails engineはプラグインの一種だが普通のRailsアプリと同様の機能(controllersやviewsやmodelsなど)を備えることができる。
  • ActionController::Liveをインクルードしてストリーミング処理を実装する。
  • クライアント・サーバ間の双方向の通信にはWebSocketsが用いられ、サーバからクライアントへの一方向のみの通信にはServer Sent Event (SSE) が用いられる。今回はSSEで実装する。
  • 通信が途切れたときは IOErrorが挙がるのでエラーハンドルしストリーミングをクローズさせる。
  • 開発環境サーバのPumaはデフォルトでは1スレッドのため、ストリーミングのテストをする時はapplication.rbで "config.allow_concurrency = true" にする。
  • Railsアプリは3つのアセットディレクトリを生成する:app/assets, lib/assets, vendor/assets
  • app/assetsにはそのアプリに直接関係あるアセットファイル、lib/assetsにはそのアプリ外でも独立して利用可能なアセットファイル、vendor/assetsにはサードパーティのファイルをそれぞれ格納する。
  • 定期的にクライアント・サーバ間で通信させることは負荷が重いので標準ライブラリの"Queueクラス"を活用する。
  • Queueクラスを使えばイベントが起こるまで通信をスリープさせることができ、また新しいイベントのストリーミングが終わったら再度スリープする。

reloadCSSが自動で適用されない?!

Rails5で実装例を試していたことが原因(?)でreloadCSSが自動で適用されないなど、いくつか上手く行かない部分があった。対処法は不明。

6. Writing DRY Controllers with Responders

この章で学ぶこと

  • Rails respondersとrespond_withメソッド
  • Rails テンプレートgeneratorのカスタマイズ

実装例

ActionController::Responderのrespond_withメソッドをカスタマイズしてHTTPキャッシングやflashメッセージも扱えるようにし、それをscaffoldデフォルト設定にする実装例。

ポイント

  • respond_withメソッドを利用する場合、その上段でrespond_toで利用するフォーマットを宣言しておく必要がある。
  • ActionController::Responderをカスタマイズする場合、以下のように定義する必要がある。

# 全体にカスタマイズを適用させる場合(Railsのconfigに書く)
ApplicationController.responder = MyAppResponder

# 特定のコントローラのみにカスタマイズを適用させる場合(例:UsersController)
class UsersController < ApplicationController
  self.responder = MyCustomUsersResponder
end
  • respond_withで呼び出されるto_htmlメソッドをオーバライドすることで、flashメッセージを挿入させる(以下引用)。

module Responders
  module Flash
    def to_html
      set_flash_message! unless get?
      super
    end

    private
    def set_flash_message!
      status = has_errors? ? :alert : :notice
      return if controller.flash[status].present?

      message = i18n_lookup(status)
      controller.flash[status] = message if message.present?
    end
    
    def i18n_lookup(status)
      namespace = controller.controller_path.gsub("/", ".")
      action    = controller.action_name
      lookup  = [namespace, action, status].join(".").to_sym
      default = ["action", action, status].join(".").to_sym
      I18n.t(lookup, scope: :flash, default: default,
             resource_name: resource.class.model_name.human)
    end
  end
end

Rails5で出るエラー

Rails5では以下のエラーがでる。Rails4.2から出るエラー?(uninitialized constant ActionController::Responder)っぽいが解決できなかった。

Gem Load Error is: uninitialized constant ActionController::Responder

7. Managing Application Events with Mountable Engines

この章で学ぶこと

  • ActiveSupport::Notifications API
  • Rails mountable and isolated engines
  • Rack, middleware stacks and custom middleware

実装例

マウンタブルで独立したエンジンアプリの作成。

ポイント

  • 独立したmount可能なエンジンアプリを作りたい時は、rails newコマンドに「--mountable」のオプションを付与する。
$ rails plugin new [アプリ名] --mountable
  • これにより、namespaceがメインアプリと切り分けられるため、エンジンのコードがオーバライドされる心配がなくなる。
  • メインアプリのルーティングパスを利用したい時は、パスの前に「main_app」を付ける。
  • RackはRailsで用いられる「ミドルウェア」の一種。
  • ミドルウェアとは、HTTPリクエストとRailsアプリの間に入り、追加的な処理を実行するもの。
  • Railsアプリで用いられてるミドルウェアのリスト(ミドルウェア・スタック)は以下のコマンドで閲覧可能。
$ rails middleware

8. Translating Applications Using Key-Value Bank Ends

この章で学ぶこと

  • I18nフレームワーク
  • Sinatraウェブフレームワーク
  • Rails router
  • Devise and Capybara (for integration testing) gems

実装例

I18n翻訳をymlファイルでなくDBのデータで実行するアプリの作成。

ポイント

  • 「rails new」しなくとも、新規作成したディレクトリに「config.ru」を作成し、必要なコードを記載していけばrailsアプリを作ることができる。
  • redisとはメモリ上にKey-Valueストア(KVS)を構築することができるソフトウェア
  • Sinatoraは、簡素なRubyウェブアプリを作成するためのDSL(domain-specific language)。Sinatoraについてはこんな記事もある『SinatraはDSLなんかじゃない、Ruby偽装を使ったマインドコントロールだ!

所感

Railsの構造について理解が深まる一冊。

実装しながら読み進めれる内容なので単純な「読み物」よりはモチベーションを保ちやすいが、難易度はやや高め。
また、Rails5ではエラーが出て、そのままサンプルを再現できないものもあった。

1、2回ざっと通読して、それ以後は、実際に類似するアプリを作成する時に該当する部分をしっかり読む、といったような使い方が良いのではと感じた。

Source

Crafting Rails 4 Applications

兵庫県西宮市生まれのフリーランスRailsエンジニア。海外を拠点にデジタルノマド生活中。/ 前職・資格:公認会計士 / プログラミング言語:Ruby, JavaScript, HTML, CSS / 日本語・英語
コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です