きにきじ

今日の気になる記事とか学びとか

『パーフェクト Ruby on Rails』を読んだ

| Comments

『パーフェクト Ruby on Rails』(すがわらまさのり, 前島真一, 近藤宇智朗, 橋立友宏)を読みました。「Rails 開発に慣れてきたかな」くらいの人にちょうどいい内容だったと思います。それくらいレベルの人が少し上を目指したり、より Rails らしい設計や開発の仕方を学んだりするのにいい書籍だと思いました。Ruby 2 や Rails 4 向けの説明になっているので、新しめの情報を得たいような場合にもお薦めです。逆に、最新の Ruby や Rails でバリバリ開発しているような人には既知のことばかりで物足りないんじゃないかなという印象です。

全体的に興味はあったのですが、購入の決め手となったのは第9章「より実践的なモデルの使い方」です。どう設計するか、どうリファクタリングするかの1つの指針として読んでみたいと思いました。実際に読んだ感想としては、学びも多く、読んでよかったと思います。

以下、特に勉強になったところ、気になったところのメモです。ちょくちょく載せているサンプルコードのうち明示していないものは自分で適当に考えたものです。本書にはより適切なサンプルコードと詳しい解説が載っていますのでぜひ購入してご覧になってみてください。

第1章 Ruby on Rails の概要

Rails を扱うための準備や概要について書かれた章です。Rails の開発に慣れている人はあまり読む必要がないと思います。

「いつも開発環境の準備とか初期の開発とかは他の人がやってくれるので自分ではほとんどやったことない」とか、「普段の環境と自宅の環境が全然違っていて、例えば職場では Mac に VMware Fusion をインストールして仮想環境で Linux OS を動かして開発しているけど自宅には Windows マシンしかない」みたいな、準備にあまり自信がないような場合は読む価値があるかもしれません。

第2章 Ruby on Rails と MVC

MVC について書かれた章です。Rails における MVC の考え方、具体的な開発イメージが掴めると思います。この章も、慣れている人はあまり読む必要がないと思います。

Rails で MVC のモデル、ビュー、コントローラがそれぞれどういう扱いなのかはざっくりわかりますが、きちんと MVC について理解したければ別の書籍やネットの情報にあたったほうがいいです。

実際の開発で使う Rails 4 のいろいろな機能について解説されているので、そのへんあまり把握してないという人は読むといいかもしれません。以下のように、Rails 3 や 4 から書式が変わったり新機能として実装されたりしたものも解説されています。リリースノート読むの面倒臭いから手っ取り早く知りたいという方にはちょうどいいかもしれません。

  • モデル
    • Arel
    • バリデーション
    • ActiveRecord enums
  • コントローラ
    • ルーティング、リソース
    • StrongParameters
  • ビュー
    • variants
    • HTML エスケープと raw メソッド

第3章 アセット

画像、JavaScript、Stylesheet などのアセットについて書かれた章です。Asset Pipeline の仕組みと活用法について書かれています。

自分は Asset Pipeline についての理解が浅く、開発でもほとんど触れたことがないため、興味深く読みました。CoffeeScript や Sass についても簡単に触れられているので、入門としてはいいんじゃないかと思います。

個人的には、テンプレートの拡張子を2つ以上重ねる Tip が新しい学びでした。例えば、test.js.coffee.erb というファイル名で、CoffeeScript のファイルに ERB を埋め込めます。

test.js.coffee.erb
1
2
3
4
<% 5.times do |i| %>
  notice<%= i %> = ->
    alert("<%= i %>")
<% end %>

それと、アセット関連の読み込み改善ということで、Turbolinks についても触れられています。概要や注意点について書かれているので、こちらも入門用としてはよさそうです。

第4章 Rails のロードパスとレイヤーの定義方法

モデル、ビュー、コントローラ以外のレイヤーを追加する方法と、その際に必要になるロードパスの考え方について書かれた章です。ワーカーやデコレーターといったレイヤーの追加の仕方を gem を紹介しながら説明しています。

個人的にはこういう設計に関するところは興味があったのでおもしろかったです。以下はメモ。

lib/autoload ディレクトリをオートロードさせるには、config/application.rb に以下のように記述します。

config/application.rb
1
2
3
4
5
class Application < Rails::Application
  config.autoload_paths += %w[#{config.root}/lib/autoload)

  ....
end

Sidekiq gem で非同期処理

Sidekiq という gem を使うと、Rails からメッセージを受け取ったワーカーが Rails とは別に立ち上げられたプロセスで処理を行うことで非同期処理を実現できます。Redis が必要です。

実装は、ワーカークラス内で include Sidekiq::Worker してインスタンスメソッド perform を定義します。perform の引数は、JSON でシリアライズ可能な値のみ使用できます。

第5章 開発を効率化する gem

デバッグの効率化やよく使うコマンドの高速化を行う gem を紹介した章です。

Pry: irb を高機能にした REPL 環境

  • ls: 実行中のスコープ内の変数やメソッドの一覧を出力
  • cd: オブジェクトの中へ移動する。対象のオブジェクトの状態を詳しく調べられる
  • show-method: メソッド定義を参照する。C 実装の部分はダメ
  • show-doc: ドキュメントを参照する。C 実装の部分はダメ
  • ブレークポイントの設定、ステップ実行(byebug gem が必要)

pry-rails を使うと、Rails console でも Pry を使えるようになります。また、以下のコマンドが追加されます。

  • recognize-path: 引数に渡した文字列などをルーティングの action や controller 情報にパースする
  • show-middleware: 読み込んでいる Rack Middleware を表示する
  • show-model: 引数に渡したモデルの情報を出力する
  • show-models: すべてのモデルの情報を出力する
  • show-routes: ルーティング情報を出力する

Hirb: コンソール上のモデルの出力を整形

ActiveRecord::Base インスタンスの出力が表形式になります。マルチバイト文字を扱う場合は hirb-unicode gem も必要です。

Pry で Hirb を利用する場合は .pryrc に設定を記述しておくと便利です。

Better Errors: エラー画面をリッチに

binding_of_caller gem を一緒に使うと、以下のようにさらに便利になります。

  • 例外が発生したときの変数の内容を出力
  • 例外が発生した時点の状態で REPL 環境が起動、オブジェクトの操作が可能に

Spring: コマンド高速化

ライブラリロードなどが短縮されるので、railsrake といったコマンドが高速化します。Windows 環境では動きません。

Rails ERD: ER 図を生成

rake タスクに erd が追加されます。実行すると、Graphviz を使って描画された ER 図が PDF で出力されます。つまり、Graphviz 必須です(R とか使っている人には定番ツールですね)。

第6章 Rails アプリケーション開発

実際の Rails アプリケーション開発のチュートリアルです。RESTful なルーティング、Bootstrap の導入、OAuth 利用(Twitter でログイン)、エラーハンドリング、Kaminari gem + kaminari-bootstrap gem を使ったページング(ページネーション)、ransack gem を使った検索機能、carrierwave gem を使った画像アップロード機能について書かれています。

気になった点について触れていくと……

まず、OAuth のところで出てきた、「lvm.me」については初めて知りました。

AbstractController::Helpers::ClassMethods#helper_method を使ってコントローラからヘルパーのメソッドを呼び出せるようにできるのは忘れがちです。

./bin/rails g resources でリソースを作成すると、config/routes.rb 内に自動で resources:events が追加されます。

あと、ActiveRecord::Base.find_by! のように ! をつけると見つからなかったときに例外を発生させられるんですね(古い Rails で提供されていた find_by_name のような Dynamic Finder も同様)。恥ずかしながら知りませんでした……。見つからなかったときに例外を吐くのは find くらいかと思っていました……。

ransack gem は使ってみたい gem の1つです。where を使っていたところを search に置き換えて、あとはパラメータ名を工夫するといい感じの SQL を発行してくれます。クラスメソッドの ransackable_attributesransackable_associations を使えば検索条件や関連を限定することもできます。

1
2
Event.search(name_cont: "JavaScript")    # equal to Event.where("name like ?", "%JavaScript%")
Event.search(start_time_gteq: Time.now)  # equal to Event.where("start_time >= ?", Time.now)

画像アップロード機能のところでは、以下のような、画像を扱うときの注意点についても触れられていて参考になります。carrierwave-magic gem などを使うと mime-type を判定したりもできるのですね。

  • アップロードしたファイルをどこに配置するか
  • サイズが大きい画像をアップロードした場合にどうするか
  • 画像以外のファイルをアップロードした場合はどうするか

最後に、Rails を使って開発をする際に意識しておくべき大切なことが書かれています。

……(Rails が提供する)機能を学ぶ際は、単純に丸暗記するのではなく「なぜこのような機能があるのか」を積極的に調べるようにしましょう。Rails が提供している機能の多くは、Web アプリケーション開発全般で使えるベストプラクティスです。

……たいていの場合、実装方法の選択肢は複数存在します。選択肢の種類を増やすことと、その中から適切な実装方法を選ぶこと、両方とも経験が必要なことですが、大事なのは常に複数の選択肢があることを前提に、設計に真摯に向き合うことです。

第7章 Rails アプリケーションのテスト

Rails におけるテストの書き方、テストで使えるツール、TDD(テスト駆動開発)について書かれた章です。minitest と RSpec の比較なんかもあり、どういう場合にどちらが適切かを考える1つの指針になるかもしれません。capybara gempoltergeist を使ったエンドツーエンドのテストについても書かれています。CI、カバレッジ、静的解析(e.g., Brakeman gemrails_best_practices gemCode Climate というサービス)にも少し触れられています。

本章では、テストを書く理由について以下のように述べられています。

  • テスト対象となる仕様とアプリケーションの設計を考える機会が増える
    • (仕様の抜け漏れを減らすことができる)
    • (きれいな設計にできる)
  • 手作業によるテストを減らすことができる
  • 「ここはちゃんと想定どおりに動くだろうか……」という不安を減らせる
  • リファクタリングや仕様の変更に自信を持って対応できる

個人的に加えるとすれば、「仕様書代わりにできる」という点でしょうか。普段テストを読むというのはそこまでやりませんが、あるクラスやメソッドについて理解を深めたいときにテストを読んで仕様を把握しようとすることがあります。特に RSpec を使うとより Spec を記述しやすいのでこのポイントを意識しています。

本の内容に戻りますが、RSpec を使うなら shoulda-matchers gem を使うと幸せになれそうですね。

ダブル(他のオブジェクトの代わりをするオブジェクト)、スタブ(メソッドを仮に定義し、メソッドが実行されたときに任意の値を返させる)についても触れられています。ダブル、スタブ、モックの違いについてはいろいろ情報があるのでそちらに譲ります(ちなみにこのへんの違いは本書を読んでも詳しくはわかりません)。

assign メソッドでインスタンス変数に値を設定できます。ただ、@hoge = fuga と書いたり instance_variable_set したりするのとの違いがわかっていません。そのうち調べます。

capybara はこれまで使ったことがありませんでした。エンドツーエンドのテストに関して個人的に気になったポイントは以下です。

  • capybara は、ドライバと呼ばれる仕組みによって、「どのブラウザを利用してテストをするのか」を切り替えられる
  • capybara は selenium(ブラウザを操作してテストするドライバ)をサポートしているが、実際にはあまり行われていない
    • 実行時間がかかりすぎるため
    • ブラウザを動かすための GUI 環境が必要なため。代わりに、capybara-webkit や poltergeist というドライバを利用するのが一般的
  • 最近はコントローラやビューのテストの記述は少なくなる傾向にある。ただし、エンドツーエンドのテストは実行時間がかかるので、内容からどこに書くべきかを判断して書きわけることが大切
  • Database Cleaner gem で、DatabaseCleander.strategy を切り替えることで、通常のテストではトランザクションを利用し、poltergeist を利用したテストでは truncation でデータ削除するという使いわけができる
  • describe の第2引数に js: true を渡すことで、capybara に対して JavaScript 用のドライバを使うことを知らせる

第8章 Rails のインフラと運用

サーバの構築や構成管理、アプリケーションの配備(デプロイメント)、監視について書かれた章です。Vagrant + Chef を使った環境の構築方法、Capistrano の使い方、New Relic の使い方が書かれています。

他にも、以下のようなツール/サービスが紹介されています。

第9章 より実践的なモデルの使い方

“Skinny Controllers, Fat Models”(コントローラを薄く、モデルを厚く)に基づいて Rals 開発を進めていくと、どんどんモデルが肥大化します。この問題に対して、バリデーションおよびコールバックを小さなクラスに分離する方法、ActiveModel を使って RDB(リレーショナルデータベース)に依存しないモデルクラスを定義する方法、一部の機能を ActiveRecord モデルの外に抽出する方法が紹介されています。

個人的には、本書の中で最も読みたかったところです。ある程度はネット上に情報があるのですが、Rails で具体的にどうやるかがまとまっているのがありがたかったです。

コールバック、バリデーションを小さなクラスに分離する

コールバックの分離

コールバックをクラスに分離するメリットは本書によれば以下のとおりです。

  • コールバックが複数のフックポイントにまたがって1つの機能を実現している場合、それぞれのコールバックの関係性を明確に表現できる
  • モデルクラスは本来のビジネスロジックの実装に集中できる
  • 機能の境界がはっきりすることで、テストを行いやすくなる

コールバックに渡すことのできるオブジェクトは、そのコールバックの名前と同じ名前のインスタンスメソッドを実装している必要があります。本書にも引用されている ActiveRecord::Callbacks のコード例は以下です。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class BankAccount < ActiveRecord::Base
  before_save      EncryptionWrapper.new("credit_card_number")
  after_save       EncryptionWrapper.new("credit_card_number")
  after_initialize EncryptionWrapper.new("credit_card_number")
end

class EncryptionWrapper
  def initialize(attribute)
    @attribute = attribute
  end

  def before_save(record)
    record.send("#{@attribute}=", encrypt(record.send("#{@attribute}")))
  end

  def after_save(record)
    record.send("#{@attribute}=", decrypt(record.send("#{@attribute}")))
  end

  alias_method :after_initialize, :after_save

  private
    def encrypt(value)
      # Secrecy is committed
    end

    def decrypt(value)
      # Secrecy is unveiled
    end
end

バリデーションの分離

バリデーションを分離するメリットは、本書では以下のように述べられています。

  • 責任の境界がはっきりする
  • テストが容易になる

ActiveModel::EachValidator を継承したクラスを定義すると、モデルで使う validates メソッドがそのクラス名に基づいたオプションを受け取れるようになります。その際の決まり事は以下です。

  • バリデータクラスにインスタンスメソッド validate_each を定義する
  • validate_each の引数はモデルのインスタンス、属性名、属性値の3つ
  • HogeValidator というクラス名の Validator を除いた部分を underscore したものがオプション名となる。HogeValidator なら hoge

既存のバリデータクラスを継承したサブクラスを定義したり、新しく自分でバリデータクラスを定義したりすることで、うまく検証ルールを整理できます。

以下では、コメントの内容に必ず褒め言葉を入れるように強制しています(……ツッコミどころ満載な例ですが)。

lib/autoload/must_praise_validator.rb
1
2
3
4
5
6
7
class MustPraiseValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
    unless value =~ /すばらしい|さすが|感動|ありがとう/
      record.errors.add attribute, (options[:message] || "は必ず褒めてください。")
    end
  end
end
app/models/comment.rb
1
2
3
class Comment < ActiveRecord::Base
  validates :content, presence: true, must_praise: true
end

ActiveModel::Validator クラスを継承すると、1つの属性値だけに留まらない複雑な検証ルールを扱うクラスを定義できます。このクラスは以下のような構成となります。

  • インスタンスメソッド validate が定義されている
  • validate メソッドの引数はモデルのインスタンスのみ
  • 実際に使うモデルのクラスでクラスメソッド validates_with に引数としてバリデータクラスを渡す

以下は、Event クラスの開始時刻と終了時刻が入力された場合に正しく時間(範囲)として扱えるかを検証しています。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# == Schema Information
#
# Table name: events
#
# id          :integer     not null, primary key
# name        :string(255)
# start_time  :datetime
# finish_time :datetime
# created_at  :datetime
# updated_at  :datetime
#
class Event < ActiveRecord::Base
  validates_with RangeableValidator, if: [:start_time, :finish_time]
end

class RangeableValidator < ActiveModel::Validator
  def validate(record)
    unless start_time < finish_time
      record.errors.add :base, "終了時刻は開始時刻よりもあとにしてください。"
    end
  end
end

クラスを分離するメリット、デメリット

コールバックやバリデーションをクラスに分離するデメリットは以下です。

  • コード量が増加する
  • 実装が分散することで、モデルクラスの動作を把握する手間が増える

一方で、分離するメリットというか使いどころは以下です。

  • モデルクラスの制約から分離する: テストが容易になる。過剰なトリックを使わずに機能の本質をテストできる
  • 業務知識を表現する: 業務上のルールとして重要な場合、そのルールに相当する名前で分離することにより、業務上の概念と設計上の概念を一致させられる

注意点として、対象となる制約条件がどの範囲で利用されるのかを踏まえた上で記述方法を考えることが大切という点にも触れられています。ある条件のときに一部のバリデーションやコールバックだけを無効にするというのは手間がかかり複雑になります。特定の機能についてのみ影響するような制約条件については、バリデーションやコールバックとして表現するのではなくサービスクラス(後述)の中にまとめてしまったほうがいい場合もあります。

また、分離する以外にも、特定の機能についての関心事をモジュールにまとめて切り出して表現することもできます。この方法は後述の Concern で詳しく記述されています。

ActiveModel: データベースに依存しないモデルをつくる

データベースには対応しないけれど以下のような ActiveRecord の便利な機能を持ったクラスを定義したい場合、ActiveModel モジュールを使うと便利です。

  • バリデーション
  • コールバック
  • 属性名を基にした動的なメソッドを定義
  • 属性値に対する変更を保持する
  • オブジェクトのシリアライズ

本書では、ActiveModel の機能の中で代表的なものについて紹介しています。

ActiveModel::AttributeMethods

ActiveModel::AttributeMethods は attr_accessor などで定義したアクセサメソッドに対しメソッドを宣言的に定義できる機能を提供します。

ActiveModel::AttributeMethods を include すると、以下のクラスメソッドが定義されます。

  • attribute_method_suffix
  • attribute_method_prefix
  • attribute_method_affix
  • define_attribute_methods
  • alias_attribute

例えば以下のように定義したとすると、 以下のメソッドが使えるようになります。

  • Person#upcase_first_name
  • Person#upcase_family_name
  • Person#upcase_first_name!
  • Person#upcase_family_name!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Person
  include ActiveModel::AttributeMethods

  attribute_method_prefix "upcase_"
  attribute_method_affix  prefix: "upcase_", suffix: "!"
  define_attribute_methods :first_name, :family_name

  attr_accessor :first_name, :family_name

  def attributes
    {
      "first_name"  => @first_name,
      "family_name" => @family_name,
    }
  end

  private
    def upcase_attribute(attr)
      send(attr).upcase
    end

    def upcase_attribute!(attr)
      send("#{attr}=", upcase_attribute(attr))
    end
end

person = Person.new
person.first_name  = "Jonathan"
person.family_name = "Joestar"
person.upcase_first_name   #=> "JONATHAN"
person.upcase_family_name  #=> "JOESTAR"

ActiveModel::Callbacks

ActiveModel::Callbacks は before_saveafter_create のようなコールバックを定義しやすくしてくれるモジュールです。ActiveRecord と同様の記述スタイルでいいのであれば ActiveSupport::Callbacks モジュールを使うよりも記述が少なく簡単に記述できるというメリットがあります。

使い方は以下のサンプルコードを見ればだいたいわかると思います。毎回 run_callbacks を呼び出してあげないといけないのがちょっと面倒臭いですね。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class Hero
  extend ActiveModel::Callbacks
  define_model_callbacks :appear                   # `before_appear` と `after_appear` を定義
  define_model_callbacks :defeat, only: [:before]  # `before_defeat` のみ定義

  attr_accessor :skill_name

  before_appear :transform
  before_defeat :shout_skill_name
  after_appear :lose

  def transform
    puts "変身!!"
  end

  def appear
    run_callbacks :appear do
      puts "参上!!"
    end
  end

  def lose
    puts "何……だと……"
  end

  def shout_skill_name
    puts <<-EOS
_人人人人人人人人人_
>  #{skill_name}   <
 ̄Y^Y^Y^Y^Y^Y^Y^Y^Y^ ̄
    EOS
  end

  def defeat(enemy)
    run_callbacks :defeat do
      puts "とどめだ、#{enemy.name}!!"
    end
  end
end

ActiveModel::Dirty

ActiveModel::Dirty を使うと、属性値の変化を追跡できるようになります。hoge_changed? などのメソッドが使えるようになります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class User
  include ActiveModel::Dirty

  define_attribute_methods :password

  def password
    @password
  end

  def password=(value)
    password_will_change! unless value == @password
    @password = value
  end

  def save
    @previously_changed = changes
    @changed_attributes.clear
  end
end

user = User.new
user.password_changed?  #=> false
user.changed            #=> []
user.changes            #=> {}
user.password = "hoge"
user.password_changed?  #=> true
user.changed            #=> ["password"]
user.changes            #=> { "password" => ["", "hoge"] }
user.save
user.password_changed?  #=> false
user.changed            #=> []
user.changes            #=> {}

define_attribute_methods を使用していることからわかるように、ActiveModel::Dirty は内部で ActiveModel::AttributeMethods を利用しています。

変更の追跡には hoge_will_change! メソッドが使われます。変更されたという状態をクリアする際は @changed_attributes をクリアします。前回の変更内容を保持するには @previously_changedchanges の内容を保持しておきます。

ActiveModel::Naming

ActiveModel::Naming を使うと、クラスが model_name メソッドを使えるようになります。model_name メソッドは、文字列に似た ActiveModel::Name オブジェクトのインスタンスを返し、それにより Rails の命名規約や I18n を利用した文字列の変換を簡単に処理できるようになります。

1
2
3
class User
  extend ActiveModel::Naming
end

似たモジュールとして、以下のようなものもあります。

モジュール名 内容
ActiveModel::Translation I18n を利用するためのヘルパーメソッドを定義してくれる
ActiveModel::Conversion オブジェクトを URL のパラメータとして利用したりファイルパスの検索キーとして利用しやすい形に変換してくれる

ActiveModel::Serialization

ActiveModel::Serialization は、json や xml 形式でオブジェクトをシリアライズする機能を追加します。実際にシリアライズする機能は ActiveModel::Serializers::JSON と ActiveModel::Serializers::XML に定義されているので、利用する際はこちらを include して使うことになります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Comment
  include ActiveModel::Serializers::XML

  attr_accessor :user, :article, :content, :created_at, :updated_at

  def attributes
    {
      "user"       => @user,
      "article"    => @article,
      "content"    => @content,
      "created_at" => @created_at,
      "updated_at" => @updated_at,
    }
  end

  def attributes=(hash)
    hash.each do |key, value|
      instance_variable_set("@#{key}", value)
    end
  end
end

ActiveModel::Serializer::JSON が提供する from_json および to_json メソッドを使う場合は、attributes メソッドと attributes=(hash) メソッドを定義する必要があります。

ActiveModel::Validations

ActiveModel::Validations を使うと、ActiveRecord で利用できるのとほぼ同じバリデーションの機能が使えるようになります。ただし、一部データベースにデータが保存されている前提の機能は ActiveRecord::Validations に定義されていますので、ActiveModel::Validations ですべて ActiveRecord のバリデーションと同じ機能が使えるわけではありません。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Article
  include ActiveModel::Validations
  include ActiveModel::Validations::Callbacks

  attr_accessor :author, :category, :url, :title, :content, :created_at, :updated_at

  validates_presence_of :author
  validates :url, presence: true, format: { with: /\A[-_a-z0-9]*\z/ }

  before_save :set_title, unless :title?

  private
    def set_title
      self.title = url.gsub(/[^a-z0-9]+/, " ").split(" ").map(&:capitalize).join(" ")
    end
end

ActiveModel::Validations で使えるようになるバリデーションは以下です。

  • validates_absence_of
  • validates_acceptance_of
  • validates_confirmation_of
  • validates_exclusion_of
  • validates_format_of
  • validates_inclusion_of
  • validates_length_of
  • validates_numericality_of
  • validates_presence_of
  • validates_size_of

なお、before_validation などのコールバックを使う場合は別途 ActiveModel::Validations::Callbacks の include が必要です。

ActiveModel::Model

ActiveModel::Model は Rails 4 から追加されたモジュールで、これを include すると以下のモジュールを一括で include してよりシンプルな記述で ActiveRecord ライクな挙動を与えることができるようになります。

  • ActiveModel::Naming
  • ActiveModel::Translation
  • ActiveModel::Validations
  • ActiveModel::Conversion

ActiveModel::Model を include したクラスは Rails のフォームヘルパーなどに引数として渡すために必要な振る舞いを簡単に満たせます。そのため、データベースに依存しない入力フォームなどを構築したいときや、複数の ActiveRecord オブジェクトにまたがる情報をまとめて扱いたいときなどに便利ということです。

エンティティと値オブジェクト

アプリケーションが扱うオブジェクトは、同一性をどう捉えるかによって種類にわけられます。

種類 意味
エンティティ システムにおいてオブジェクトの同一性が重要な意味を持つもの。Rails のモデルが持つ id のような識別情報を持つ。識別情報が同じなら同じエンティティ User(id, login_id, name, email, address) クラスのオブジェクト
値オブジェクト 「何である」かが重要で、値が同じであればアプリケーション上は同一とみなしていいもの。オブジェクトが持つ値が同じかどうかが同一性を決める メールアドレス、住所

ActiveRecord オブジェクトは基本的にはエンティティですが、その属性値の中には値オブジェクトとして扱うと便利なものもあります。

例えば以下のような、名前と(なぜか)和暦の日付を管理する Holiday クラスがあったとします(ねーよw)。日付は年号 era_name と年月日から成ります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# == Schema Information
#
# Table name: holidays
#
# id         :integer     not null, primary key
# name       :string(255)
# era_name   :string(255)
# year       :integer
# month      :integer
# day        :integer
# created_at :datetime
# updated_at :datetime
#
class Holiday < ActiveRecord::Base
  def same_date?(other)
    return false unless other

    same_era_name?(other) && same_year?(other) && same_month?(other) && same_day?(other)
  end

  private
    def same_era_name?(other)
      era_name == other.era_name
    end

    def same_year?(other)
      year == other.year
    end

    def same_month?(other)
      month == other.month
    end

    def same_day?(other)
      day == other.day
    end
end

日付が同じかどうかを判別する機能を Holiday クラスに直接実装すると、↑のコードように Holiday クラスが持つ責任範囲が広くなりすぎてしまいます。また、例えば誕生日(Birthday)など他のクラスにも日付を持つオブジェクトが存在するかもしれません。

このような場合、値オブジェクトとして日付を表現すると便利です。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Holiday < ActiveRecord::Base
  def date
    @date ||= JapaneseCalendarDate.new(era_name, year, month, day)
  end

  def date=(date)
    self.era_name = date.era_name
    self.year     = date.year
    self.month    = date.month
    self.day      = date.day
    @date = date
  end
end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class JapaneseCalendarDate
  attr_accessor :era_name, :year, :month, :day

  def initialize(era_name = nil, year = nil, month = nil, day = nil)
    @era_name = era_name
    @year     = year
    @month    = month
    @day      = day
  end

  def hash
    era_name.hash + year.hash + month.hash + day.hash
  end

  def ==(other)
    return false unless other.kind_of?(JapaneseCalendarDate)

    same_era_name?(other) && same_year?(other) && same_month?(other) && same_day?(other)
  end

  private
    def same_era_name?(other)
      era_name == other.era_name
    end

    def same_year?(other)
      year == other.year
    end

    def same_month?(other)
      month == other.month
    end

    def same_day?(other)
      day == other.day
    end
end

このように値オブジェクトを使うことで、JapaneseCalendarDate というコンパクトな範囲に責任を限定し、実装を適切な場所に移すことができました。もし日付を扱うクラスが増えても同じ実装を再利用すれば OK です。

値オブジェクトは、責任を適切に分割することに加えて、業務知識の語彙を実装にマッピングする点でも意味があります。これにより実装と業務知識のモデルの差を小さくできます。

なお、Rails では compose_of メソッドを使うことで簡単に値オブジェクトを扱うことができます。さきほどの Holiday クラスの例では以下のような実装になります。

1
2
3
class Holiday < ActiveRecord::Base
  compose_of :japanese_calendar_date, mapping: [%w[era_name era_name], %w[year year], %w[month month], %w[day day]]
end

compose_of はいくつかオプションをとれます。本書には各オプションの解説と以下のような IP アドレスの例も載っています。

1
2
3
4
5
compose_of :ip_address,
  class_name: "IPAddr",
  mapping: %w[ip to_i],
  constructor: Proc.new{|ip| IPAddr.new(ip, Socket::AF_INET) },
  converter: Proc.new{|ip| ip.is_a?(Integer) ? IPAddr.new(ip, Socket::AF_INET) : IPAddr.new(ip.to_s) }

compose_of を使うと宣言的に記述できるというメリットがありますが、オプションが複雑でわかりづらいというデメリットもあります。そのような場合は素直に自分で定義したほうがいいでしょう。

Concern: 関心事の分離

Rails 4 から追加された app/models/concerns や app/controllers/concerns といったディレクトリは、開発者の関心事をまとめたモジュール(Concern モジュール)を配置することが期待されています。

ある機能を実現するために必要な処理を Concern モジュールとして分離することで、モデルやコントローラから特定の機能のためにのみ利用されるメソッドやバリデーションの実装を分離できます。もしこの機能に変更が生じたとしても、その影響範囲を想定しやすくなります。

ActiveRecord を拡張するプラグインや特定の機能を提供する gem はたくさんありますが、アプリケーション特有の機能を追加したり、外部の gem にロックインされるのを避けたりといったときには Concern を使うと便利です。

ActiveSupport::Concern

Rails 内部でも使われている ActiveSupport::Concern は Concern に関連した記述を補助してくれます。例えば、

  • included メソッドにブロックを渡す機能を追加する。include されたときにフックして実行する処理を直感的に記述できる
  • クラスメソッドを追加するための記述を補助
  • モジュールの依存関係を管理する

といった機能です。個人的には、特に最後の依存関係の管理機能が便利だと思っています(自前実装でハマった経験ありorz)。

ActiveSupport::Concern にある例を見ると機能がよくわかると思います。リンク先では依存関係のところはさらっと流していますが、本書では詳しく解説されています。

また本書には以下も詳しく載っていますので、理解を深めたい人や実例が見たいという人は一見の価値ありだと思います。

  • ActiveSupport::Concern がどのような仕組みでこれらの機能を実現しているか
  • ActiveSupport::Concern を使った論理削除機能の実装例
  • バスの配車スケジュールを管理するアプリケーションで「予約」と「予約希望」が持つ共通の振る舞いを Concern モジュールに分離した話

Module#concerning

Rails 4.1 で追加された Module#concerning を使うとインラインモジュールを簡単に記述できます。

1
2
3
4
5
6
7
8
9
class User < ActiveRecord::Base
  concerning :Segment do
    included do
      scope :male, ->{ where(sex: "male") }
      scope :female, ->{ where(sex: "female") }
      scope :young, ->{ where("age < ?", 20) }
    end
  end
end

Module#concerning の主な用途は concerning メソッドが定義してあるソースコード にコメントとして書いてあります。ざっくりまとめると以下のような感じです。

  • クラスが大きくなってくると、特定の関心事として切り出せるような関連性のある処理/振る舞いが出てくる
  • 場合によっては、別ファイルに切り出すほどではない(切り出すと逆に読みづらくなる)ということがある
  • クラス内で module Hoge; ....; end; include Hoge とすれば、まとまりとして切り出して見通しがよくなる
  • Module#concerning を使うことで、切り出したときの本質的でないコードを減らして簡潔に記述できる

サービスクラス

1つの機能を完結させるために非常に複雑な処理が必要になったり、特定のエンティティや値オブジェクトに所属させると不自然になってしまう処理が出てきたりしたときは、「サービス」という概念を使うと有効な場合があります。一連の処理をサービスクラスに抽出することで、モデルが過剰に複雑になることを防ぐことができます。

本書で紹介されているサービスクラスの特徴は以下のようなものです。

  • Rails の世界で考えた場合、コントローラとモデルの中間のような存在。コントローラがユーザからのリクエストを呼び出すインターフェイスで、サービスがモデルが行う処理をとりまとめて実行するためのインターフェイス
  • 処理そのものをカプセル化し責務を分離したもの
  • 「認証サービス」や「価格計算サービス」などの機能や振る舞いそのものを表す名前を持つ
  • 適切に設計されたサービスクラスは基本的に状態を持たない。入力が同じなら同一の結果を返す
  • 業務知識と実装のメンタルモデルを一致させる。システムが対象としている業務領域の知識と照らし合わせて、より自然な形でアプリケーションを設計するための手段
  • システムが対象としている業務に結びついた名前付けができていない場合、サービスクラスを利用する対象としては不適切な可能性がある

サービスクラスの例としてよく挙げられるのが銀行口座間の振替処理の実装です。本書ではこの例を Rails で実装したら……ということでサービスクラスの解説をしています。考え方自体は Ruby や Rails に限定された話ではないので、本書のサンプルを見なくてもいいと思います。本書で見るとすると、サービスクラス用に app/services ディレクトリを切って autoload_paths に追加するあたりでしょうか。

本書では実装例を示した後に以下のようにサービスクラスを利用するメリットを説明しています。

  • 複数の役割を持つエンティティが登場し、それを1つのトランザクションとしてまとめて表現するのが自然なときなどにはサービスクラスが有効
  • あるモデルに直接実装しようとすると、トランザクションの起点をインスタンスメソッドとして配置するのは不自然
  • モデルのクラスメソッドとして実装すると、特定の機能にのみ関わる振る舞いがクラス全体で必要な振る舞いであるかのように見えてしまう
  • インスタンスメソッドにしろクラスメソッドにしろモデルに直接実装すると、機能が増えるに従ってどんどんモデルが肥大化する
  • 明らかに業務知識を表す振る舞いなので、コントローラに実装してしまったら、業務知識をモデル層に集約して変更に対する柔軟性を高めるというレイヤーどうしの独立性を失う

そして最後に、サービスクラスを利用する際の注意点にも次のように触れています。

  • 本来、システムの中核となるビジネスロジックはモデルに集約されているのが望ましい。サービスクラスを濫用すると、本来モデルにあるのが自然な振る舞いまでサービスクラスに漏れ出てしまう
  • サービスクラスは処理や振る舞いをカプセル化しているため、手続き的な記述になりがち。同じような振る舞いが分散して実装され、設計の柔軟性が失われる危険がある
  • そうなると振る舞いの再利用性が低下し、最終的にアプリケーション全体のメンテナンス性を損なうことになる
  • サービスはあくまで処理の大枠を表現するものであり、より詳細なレベルの振る舞いは各モデルが責任を持つべき

第10章 Rails を拡張する

Rack Middleware と Railtie を自分でつくり Rails を拡張する方法について書かれた章です。以下、本書に書かれている内容を簡単にまとめます。本書では豊富なサンプルコードと詳しい解説が書かれています。

Rack Middleware

Rack というのは、「たくさんのアプリケーションサーバとたくさんの Ruby フレームワークをつなぐ規約」です。Python の PSGI の規約を基に提案されたそうです。以下の規約に準拠していれば、Rack に準拠したアプリケーションです。

  • 1つの Hash オブジェクト(env)を引数にとる call メソッドがある
  • call メソッドは「ステータスコード、ヘッダーを表現した Hash、each に反応するオブジェクト」の3つの要素を持った配列を返す

Rack Middleware は、「Rack に対応したアプリケーションに機能を追加するためのミドルウェアのこと」です。以下の特徴を持ったクラスは Rack Middleware として利用できます。

  • 別の Rack アプリケーションを第1引数にとった initialize メソッドがある
  • Rack アプリケーションと同様、env をとって配列を返す call メソッドがある

ru ファイル内で use メソッドにこのクラスを渡すことで Rack Middleware を利用します。複数の use も可能です。

Rails で Rack Middleware を使う場合は、config/application.rb または config/environments/*.rb 内で use メソッドを呼び出します。

config/application.rb
1
2
3
4
5
module AwesomeEvents
  class Application < Rails::Application
    config.middleware.use AwesomeMiddleware
  end
end

有名な Rack Middleware としては以下のようなものがあります。

名前 内容
Rack::Auth::Digest Digest 認証をかける。Rack gem に標準添付
Rack::Cors Cross-Origin Resource Sharing(ドメインをまたいだ Ajax リクエスト)を実現するために必要なヘッダーを追加してくれる
Rack::Rewrite アクセスされた URL を別のものに変換する
OmniAuth さまざまな外部認証の仕組みとの連携を Rack のレイヤーで済ませてしまう

Railtie

Railtie とは、Rails 3 以降に導入された公式のプラグイン機構です。Railtie の仕組みを使うと、次のような処理を完結に実現できます(公式ドキュメント)。

  • 初期化処理(イニシャライザ)を実行する
  • ジェネレータや Rake タスクを Rails に追加する
  • Rails の設定項目を追加し、environments ごとにカスタマイズ可能にする
  • ActiveSupport::Notifications のための固有のサブスクライバを設定する

Railtie を用いたプラグインのつくり方には大きく3パターンの方法があります。

特徴
Railtie 型 Railtie が提供する基本的な機能のみを利用する
Engine 型 Railtie 型に加えて独自のコントローラやルーティング、モデルなどを提供する DeviseDoorkeeper
Mountable Engine 型 Engine 型よりもさらに独立性の高いアプリケーションをプラグインとして提供する RailsAdminRefinery CMS

Railtie 型のプラグインは rails plugin new hoge で雛形をつくれます。

さらに lib/hoge/railtie.rb で設定を行います。本書の例では ネームスペースを切って設定項目を追加するのと、Rails のモジュールを先読みする対象のリストである eager_load_namespaces への追加を行っています。Rails のコアライブラリが読み込まれたあとに走るべき処理は initializer というブロックを用いて定義します。ActiveSupport.on_load ブロックを使うと、Rails の各コアコンポーネントの読み込みにフックして実行されるべき処理内容を記述できます。

Engine 型のプラグインは rails plugin new hoge --full のように --full オプションをつけることで雛形をつくれます。

さらに以下のような lib/hoge/engine.rb をつくれば OK です。

lib/hoge/engine.rb
1
2
3
4
module Hoge
  class Engine < ::Rails::Engine
  end
end

Mountable Engine 型のプラグインは rails plugin new hoge --mountable のように --mountable オプションをつけることで雛形をつくれます。

さらに Engine 型と似ていますが以下のような Engine ファイルをつくります。Hoge 以下のネームスペースに定義されたアプリを独立した Rails アプリケーションとして利用するという意味です。

1
2
3
4
5
module Hoge
  class Engine < ::Rails::Engine
    isolate_namespace Hoge
  end
end

コントローラなども app/controllers/hoge/*_controller.rb のようにネームスペース以下に生成されます。

他の Rails アプリで使う場合、config/routes.rb で以下のように mount 宣言をします。

1
mount Hoge::Engine => "/status"

Comments