はじめに
先日、LTイベント「Qiita Night~Rails~」にて登壇させていただきました。
そこで、Railsで大規模Webアプリケーションを 開発するときに知っておきたいテクニックを Qiita の Rails を例にいくつか紹介しました。発表資料はこちらです。
アーカイブ動画もあります。
この記事では、LTで紹介したテクニックの1つ「Iteratorパターン」について、どのように Rails に活用できるかを補足・紹介します。
Iterator パターン
Iterator パターンは、GoF の23個のデザインパターンの一つです。
Wikipedia では、「コンテナオブジェクトの要素を列挙する手段を独立させることによって、コンテナの内部仕様に依存しない反復子を提供することを目的とする。」と書かれています。
Iterator パターンについては様々な解説記事がすでにあると思うので説明は省きますが、簡単に言うと、「Array や Hash などの複数の要素を持つオブジェクトに対し、それぞれの要素を1つずつ捜査する方法を提供するパターン」です。文章で説明するよりも実際にコードを見た方がわかりやすいと思うので、実装例を紹介します。
Ruby で Iterator パターンを実装してみる
Iterator パターンについて完全に理解しているわけではないので、本来の Iterator パターンと少し異なる部分があるかもしれないです。ご容赦ください。
Ruby には Enumerable
モジュールという、繰り返しを行なうクラスのための Mix-inが用意されています。
Enumerable
を include して、 each
メソッドを実装するだけ、filter
や sort
など、繰り返しに関する様々なメソッドが利用できます。試しに作ってみましょう。
例として、[1,2,3,4]
というArray のそれぞれの要素を走査できるクラス SampleIterator
を作成してみます。
Array はすでに Enumerable を include しているので、実際には不要ですが、今回は例のためあえて Enumerable を include した新たなクラスを実装してみます。
item
メソッドで [1,2,3,4]
にアクセスできるクラスを例とします。
class SampleIterator
def items
[1,2,3,4]
end
end
これに対し、Enumerable を include して、 each メソッドも定義します。
class SampleIterator
include Enumerable
def each
items.each do |item|
yield item
end
end
def items
[1,2,3,4]
end
end
これで完成です。使ってみましょう!
irb(main):004:0> sample_iterator = SampleIterator.new
=> #<SampleIterator:0x00007fa280d12368>
irb(main):005:0> sample_iterator.filter(&:even?)
=> [2, 4]
irb(main):006:0> sample_iterator.sort_by { |n| -n }
=> [4, 3, 2, 1]
これで、filter
や sort_by
などを呼ぶことができました!
Rails で Iterator パターンを活用する
LTでも紹介しましたが、大規模な Rails ではその複雑さから Fat Model や Fat Controller が起こりやすいです。この問題に対抗するには、よくある複雑になりやすいパターンを見つけ、解決方法を決めておくことが重要です。
Iterator パターンで解決するのは、「複雑な条件のコレクションの作成」です。
例として、「Qita のタイムラインページに表示する記事」のクラスを作ってみます。
タイムラインページは、以下の条件を満たした記事を表示すると仮定します。
- 時系列順に表示する
- フォローしているユーザーの記事を表示する
- フォローしているOrganizationの記事を表示する
- フォローしているタグの記事を表示する
- ミュートしているユーザーの記事は表示しない
- ミュートしているタグの記事は表示しない
- 「フォローしているユーザーの記事のみ」などで絞り込みができる
これを先ほど紹介した Enumerable を使用した Iterator パターンで実装すると、以下のようになります。
実際の Qiita のコードとは異なります。「条件が多いと行数が多くなる」というのを表現するために雑に書いたコードです。
class TimelineBuilder
include Enumerable
def initialize(user, scope)
@user = user
@scope = scope
end
def each
articles.each do |article|
yield article
end
end
private
attr_reader :user, :scope
def articles
# scope に応じて対象とする記事を切り替える
articles = {
all: all_articles,
user: following_users_articles,
organization: following_organization_articles,
tag: following_tags_articles
}[scope]
# ミュートしているユーザーやタグの記事は非表示にする
filtered_articles = articles.filter do |item|
filters.all? { |filter| filter.valid?(article) }
end
# 時系列順にソートする
filterd_articles.sort_by { |article| -article.created_at }
end
def all_articles
[
following_users_articles,
following_organization_articles,
following_tags_articles,
].flatten
end
def following_users_articles
Article.joins(:users).merge(user.following_users)
end
def following_organization_articles
Article.joins(:organizations).merge(user.following_organizations)
end
def following_tags_articles
Article.joins(:tags).merge(user.following_tags)
end
def filters
[
UserMuteFilter.new(user),
TagMuteFilter.new(user),
]
end
end
そして、コントローラーでは以下のように呼ぶだけでOKです!
class HomeController
def timeline
require_login
@articles = TimelineBuilder.new(current_user, params[:scope] || :all)
render
end
end
このように、条件が多かったり、複雑だったりするArrayを生成する必要がある処理は、読みやすく書こうとするとメソッドの数が次々と増えていきます。これらをモデルやコントローラーに実装すると、行数が膨大になったり、どこからどこまでが今回の処理に関係あるメソッドなのかがわかりにくくなってしまいます。
そこで、今回紹介した Iteratorパターンを活用することで、タイムラインに関する処理を集約でき、Fat Modelや Fat Controller を回避することができます。
今回紹介した例のタイムラインのように、多くの条件を満たしたアイテムだけ集めて View に渡すというパターンは現実世界の問題としてよく起きると思います。 Qiita でもこのパターンが多いのですが、そういった場合は上記のようにクラスを作成すればよいので、迷うことなくFat Model や Fat Controller を回避することができます。ぜひ活用してみてください!
おわりに
この記事では、大規模 Rails で役立つテクニックの一つとしてIteratorパターンを紹介しました。LTにて紹介した別のテクニックも同様に解説記事を書く予定なので、そちらもどうぞ!