Railsで使われる代表的なデザインパターンとその特徴

この記事について

本記事は、2023年7月に社内で実施した勉強会の内容を基に、社外向けに再編集したものです。
記載の内容は執筆当時の情報であり、現在の仕様やベストプラクティスと異なる可能性があります。
実装にあたっては、必ず最新の公式ドキュメントをご確認いただくようお願いいたします。

目次

目的

Railsで開発していると、いわゆる「デザインパターン」の名前を目にする機会があると思います。その由来を知っておくことで、既存コードの設計意図の誤解を避けたり、認知コストを下げられるのではないでしょうか。

「デザインパターン」の由来

クリストファー・アレグザンダーの提唱した「パタン・ランゲージ(pattern language)」という建築設計についての考え方が、ソフトウェア開発に応用されたそうです。

  • 1977年 Christopher Alexander他著 『A Pattern Language: Towns, Buildings, Construction』
  • 1994年 Erich Gamma他著(GoF) 『Design Patterns: Elements of Reusable Object-Oriented Software』

参考記事

Yuji Ariyasu, 「Rubyのデザインパターンまとめ」, Qiita,
https://qiita.com/yuji_ariyasu/items/588fef6062b3c7149509

Iteratorパターン

コレクションの内部構造に関心を持たずに(依存せずに)、要素を一つずつ取り出すことができます。

[1, 2, 3].each do |i|
  p i
end

{ a: 1, b: 5, c: 10 }.each do |i|
  p i
end

(3..7).each do |i|
  p i
end

(10..).each do |i|
  p i
end

for i = 0 i < 10 in [0, 1, ..., 10] do print list[i] end

Enumerable について

Rubyで開発するとき、 Enumerableのメソッド一覧に目を通しておくと良いと思います。多くのメソッドが定義されており、より直截的なコードで書けるようになっています。

Mixin先の例:

  1. ActiveRecord (Rails)
  2. Array(配列)
  3. Hash(ハッシュ)
  4. Range(範囲)
  5. Set(集合)
  6. IO(入出力)- 行単位
  7. Dir(ディレクトリ)- ディレクトリのエントリ

APIライブラリでは、ページング機能などをEnumerableインターフェイスで実装している場合もあります。

Enumerableを実装するとき

Enumerable の多くのメソッドは、#each を実装するだけで利用可能です。

class MyEnumerable
  include Enumerable

  def initialize
    @values = [1, 2, 3, 4, 5]
  end

  def each
    @values.each do |value|
      yield value
    end
  end
end

e = MyEnumerable.new

e.each do |v|
	p v
end

e.map { |v| v * 2 }
=> [2, 4, 6, 8, 10]

> e.find { |v| v > 4 }
=> 5

> e.select(&:even?)
=> [2, 4]

> e.reject(&:even?)
=> [1, 3, 5]

> e.count
=> 5

> e.all?(&:even?)
=> false

> e.any?(&:even?)
=> true

> e.one? { |v| v < 2 }
=> true

> e.include?(5)
=> true

> e.max
=> 5

> e.min
=> 1

> e.minmax
=> [1, 5]

> e.sum
=> 15

> e.sort
=> [1, 2, 3, 4, 5]

> e.uniq
=> [1, 2, 3, 4, 5]

> e.take(4)
=> [1, 2, 3, 4]

> e.compact
=> [1, 2, 3, 4, 5]

> e.partition(&:even?)
=> [[2, 4], [1, 3, 5]]
例: 木構造
class TreeNode
  include Enumerable

  attr_accessor :value, :children

  def initialize(value)
    @value = value
    @children = []
  end

  def <<(child_node)
    @children << child_node
  end

  def each(&block)
    yield self
    @children.each { |child| child.each(&block) }
  end
end

# 木構造を作成
root = TreeNode.new('A')
b = TreeNode.new('B')
c = TreeNode.new('C')
d = TreeNode.new('D')
e = TreeNode.new('E')
f = TreeNode.new('F')

root << b
root << c
b << d
b << e
c << f

# イテレーション
root.each { |node| puts node.value }
A
B
D
E
C
F

root.find { |node| node.value == "E" }
=> #<TreeNode:0x00007f0860b85078 @children=[], @value="E">

root.find { |node| node.value == "G" }
=> nil

root.min_by(&:value).value
=> "A"
root.max_by(&:value).value
=> "F"
例: フィボナッチ数列
#take#take_while を使用することができる。無限ループに注意が必要。
class Fibonacci
  include Enumerable

  def each
    return enum_for(:each) unless block_given?

    a, b = 0, 1
    loop do
      yield a
      a, b = b, a + b
    end
  end
end

f = Fibonacci.new
=> #<Fibonacci:0x00007fda2ecd44d8>

f.take(5)
=> [0, 1, 1, 2, 3]

f.take_while { |n| n < 300 }
=> [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233]

f.map { |n| n * 10 }.take(5)
=> (無限ループ)

f.lazy.map { |n| n * 10 }.take(5).force
=> [0, 10, 10, 20, 30]

f.lazy.select(&:even?).take(10).force
=> [0, 2, 8, 34, 144, 610, 2584, 10946, 46368, 196418]


(0..10).lazy.map { |n| n * 10 }.take(5).force

(参考)ActiveRecordによる #each の実装

https://github.com/rails/rails/blob/593893c901f87b4ed205751f72df41519b4d2da3/activerecord/lib/active_record/result.rb#L67C1-L73C8

Enumerator (外部イテレータ)

C++ や Java で一般的に利用される形式のイテレータを取得することもできます。
ジェネレーターのような用途にも活用することが可能だと考えられます。

e = (0..5).each  # get Iterator

loop do
  begin
    p e.next
  rescue StopIteration
    break
  end
end
0
1
2
3
4
5

Template Methodパターン

RubyのEnumerableは#eachを利用してさまざまなメソッドを定義していますが、 #each自体の実装はMixin先のクラスに委ねられています。

このように、処理の骨格を定義し、一部のステップの実装を別の場所に委ねるパターンをTemplate Methodパターンと呼ぶそうです。

(「デザインパターン」においてMixinと継承をどう区別するかは、私には判断しかねます)

Decoratorパターン

オブジェクトを別のオブジェクトでラップして機能を拡張しつつ、元のインターフェースを透過的に呼び出します。

外側のオブジェクトを飾り枠でに見立て、 Decorator と呼ぶそうです。

Railsにおいては、ViewのロジックをModelから分離する gem 「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 long_title
    "#{object.title} #{object.published_at}"
  end
end

Article.find(1).title                # => 'エンジニア勉強会'
Article.find(1).decorate.title       # => 'エンジニア勉強会'
Article.find(1).decorate.long_title  # => 'エンジニア勉強会 2023/07/05'

Strategyパターン

同じ目的を達成する複数のアルゴリズム(例: プロバイダごとの認証プロセス)を独立したオブジェクトとして定義し、状況に応じて交換可能にするパターンです。

gem 「OmniAuth」の認証プロバイダgemについてReadmeなどを読むと、まさにそのまま「Strategy」という言葉が出てきます。

# Gemfile
gem 'omniauth'
gem 'omniauth-google-oauth2'
gem 'omniauth-apple'

# config/initializers/omniauth.rb
Rails.application.config.middleware.use OmniAuth::Builder do
  provider :google_oauth2, 'YOUR_GOOGLE_CLIENT_ID', 'YOUR_GOOGLE_CLIENT_SECRET'
  provider :apple, 'YOUR_APPLE_CLIENT_ID', '', { scope: 'name email' }
end
役に立ったらシェアしていただけると嬉しいです
  • URLをコピーしました!
  • URLをコピーしました!
目次