大規模 Rails で役立つテクニック: Iteratorパターン編

# はじめに

先日、LTイベント「Qiita Night～Rails～」にて登壇させていただきました。

https://increments.connpass.com/event/297116/

そこで、Railsで大規模Webアプリケーションを 開発するときに知っておきたいテクニックを Qiita の Rails を例にいくつか紹介しました。発表資料はこちらです。

<script async class="speakerdeck-embed" data-id="09d7bd2986104ccbadafb879caae3b61" data-ratio="1.77777777777778" src="//speakerdeck.com/assets/embed.js"></script>

アーカイブ動画もあります。

<iframe width="560" height="315" src="https://www.youtube.com/embed/baynYVfPdhc" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" loading="lazy" allowfullscreen></iframe>

この記事では、LTで紹介したテクニックの１つ「Iteratorパターン」について、どのように Rails に活用できるかを補足・紹介します。

# Iterator パターン

Iterator パターンは、GoF の23個のデザインパターンの一つです。
Wikipedia では、「コンテナオブジェクトの要素を列挙する手段を独立させることによって、コンテナの内部仕様に依存しない反復子を提供することを目的とする。」と書かれています。

https://ja.wikipedia.org/wiki/Iterator_%E3%83%91%E3%82%BF%E3%83%BC%E3%83%B3

Iterator パターンについては様々な解説記事がすでにあると思うので説明は省きますが、簡単に言うと、「Array や Hash などの複数の要素を持つオブジェクトに対し、それぞれの要素を1つずつ捜査する方法を提供するパターン」です。文章で説明するよりも実際にコードを見た方がわかりやすいと思うので、実装例を紹介します。

# Ruby で Iterator パターンを実装してみる

:::note warn
Iterator パターンについて完全に理解しているわけではないので、本来の Iterator パターンと少し異なる部分があるかもしれないです。ご容赦ください。
:::

Ruby には `Enumerable` モジュールという、繰り返しを行なうクラスのための Mix-inが用意されています。

https://docs.ruby-lang.org/ja/latest/class/Enumerable.html

`Enumerable` を include して、 `each` メソッドを実装するだけ、`filter` や `sort` など、繰り返しに関する様々なメソッドが利用できます。試しに作ってみましょう。

例として、`[1,2,3,4]` というArray のそれぞれの要素を走査できるクラス `SampleIterator` を作成してみます。

:::note
Array はすでに Enumerable を include しているので、実際には不要ですが、今回は例のためあえて Enumerable を include した新たなクラスを実装してみます。
:::

`item` メソッドで `[1,2,3,4]` にアクセスできるクラスを例とします。

```rb
class SampleIterator
  def items
    [1,2,3,4]
  end
end
```

これに対し、Enumerable を include して、 each メソッドも定義します。

```rb
class SampleIterator
  include Enumerable

  def each
    items.each do |item|
      yield item
    end
  end
  
  def items
    [1,2,3,4]
  end
end
```

これで完成です。使ってみましょう！

```rb
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 パターンで実装すると、以下のようになります。

:::note warn
実際の Qiita のコードとは異なります。「条件が多いと行数が多くなる」というのを表現するために雑に書いたコードです。
:::

```rb
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です！

```rb
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にて紹介した別のテクニックも同様に解説記事を書く予定なので、そちらもどうぞ！

# 参考文献

- [デザインパターン | TECHSCORE(テックスコア)](https://www.techscore.com/tech/DesignPattern/)