この記事について
本記事は、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先の例:
- ActiveRecord (Rails)
- Array(配列)
- Hash(ハッシュ)
- Range(範囲)
- Set(集合)
- IO(入出力)- 行単位
- 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
の実装
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