Rubyでのデザインパターンの使用例を説明する!!

こんにちは、サーバーサイドエンジニアの平井です。

こちらは、Enigmo Advent Calendar 2019 、24日目の記事です。

昨年の1月にエニグモインターンとして入社してから一年が経とうとしています。早いもので、新卒の肩書きもそろそろ無くなってしまいますね。

今回は、Rubyによるデザインパターンを読んで、デザインパターンを勉強したので、そのアウトプットをさせていただきます。 タイトルの通り、デザインパターンについて実際の使用例を探してみました。そのパターンと使用例は以下になります。

まずは、Strategyパターンから説明します。

Strategyパターン

Strategyパターンとは

メソッドの中に溶け込んでいるアルゴリズムを別クラスとして分けて、切り替えができるようにするパターンです。 サンプルコードを利用し、悪い例を修正していく形で、さらに、Strategyパターンについて説明していきます。

適用前
class Report
  def initialize
    @title = '月次報告'
    @text = [ '順調', '最高の調子' ]
  end

  def output_report(format)
    if format == :plan
      puts("#{title}")
    elsif format == :html
      puts("<title>#{@title}</title>")
    else
      raise "Unknown format: #{format}"
    end

    @text.each do |line|
      if format == :plan
        puts(line)
      else
        puts(" <p>#{line}</p>")
      end
    end
  end
end

問題点としては以下が挙げられます。

  • output_reportメソッドについて、formatの種類が増えるにつれて、メソッドが長くなる。

Strategyパターンを適用すると、以下のようになります。

class HTMLFormatter
  def output_report(title, text)
    puts("<title>#{@title}</title>")
    text.each do |line|
      puts(" <p>#{line}</p>")
    end
  end
end

class PlainTextFormatter
  def output_report(title, text)
    puts("#{title}")
    text.each do |line|
      puts(line)
    end
  end
end

class Report
  def initialize(formatter)
    @title = '月次報告'
    @text = [ '順調', '最高の調子' ]
    @formatter = formatter
  end

  def output_report
    @formatter.outpout_report(@title, @text)
  end
end

report = Report.new(HTMLFormatter.new)
report.ouput_report

このように、同じ目的(レポートをフォーマットする)を持ったオブジェクトを、ストラテジとして定義し、そのストラテジをごっそり交換できるようにするのがStrategyパターンです。ストラテジは同じインターフェースを持っているので、ストラテジの利用者(コンテキストと呼ぶそうです)は、どのストラテジを利用するかを知らずにすみます。適用前は、ReportクラスはHTMLとPlainText、それぞれの出力方式を知っていました。ただ、Strategyパターンを適用した後は、出力方式に関する知識はReportクラスからストラテジオブジェクトに移りました。

実例

Strategyパターンは、Wardenという認証フレームワークで使われています。

github.com

Warden::Strategies::Baseのサブクラスに認証の方法を実装することで、その認証方法を切り替えることができます。 そのWardenを使った、Deviseというgem(ログイン方法を簡単に実装できる)のソースコードを見ると、実際にどのようにStrategyパターンが使われているのかを確認できます。 下のソースコードを見るとわかるように認証するオブジェクトはwardenですが、認証方法に関する知識はDatabaseAuthenticatableクラスRememberableクラスが持っています。

module Devise
  module Strategies
    class DatabaseAuthenticatable < Authenticatable
      def authenticate!
        # データベースにあるデータで認証を行う処理
      end
    end
  end
end
module Devise
  module Strategies
    class Rememberable < Authenticatable
      def authenticate!
        # クッキーを使って認証を行う処理
      end

DatabaseAuthenticatableは、データベースにあるメールアドレスとパスワードで認証します。Rememberableは、クッキーからデータベースにあるレコードを探してきて認証します。

Observerパターン

Observerパターンとは

あるオブジェクトの状態の変化を、そのオブジェクト自身がその変化を知りたいオブジェクトに対して知らせる仕組みがObserverパターンです。 システムの各部分が、あるオブジェクトの状態を知りたいとき、例えば、誰かの給料が変わった時に、その変更を経理部門が知る必要がある時を想定します。 悪いパターンのサンプルコードを見ていきます。

class Payroll
  def update(changed_employee)
    puts("#{changed_employee.name}の給料が#{changed_employee.salary}に変わりました")
  end
end

class Employee
  attr_reader :name, :employee

  def initialize(name, title, salary, payroll)
    @name = name
    @title = title
    @salary = salary
    @payroll = payroll
  end

  def salary=(new_salary)
    @salary = new_salary
    @payroll.update(self)
  end
end

payroll = Payroll.new
taro = Employee.new('Taro', 'President', 200, payroll)
taro.salary = 300

このソースコードの悪い点は以下になります。

  • 他のオブジェクトが、財務情報を知る必要が出た時に、実際に変化したのは、Employeeではなく他のオブジェクトなのにも関わらず、Employeeクラスを修正する必要がある。

Rubyによるデザインパターンに書かれていた設計原則として、変わるものを変わらなものから分離すると述べられています。この原則を当てはめて、変わるもの(誰がtaroの財務状況を知る必要があるか)を変わらないもの(Employeeクラス)から分離させます。 Observerパターンを当てはめると以下のようになります。

class Payroll
  def update(changed_employee)
    puts("#{changed_employee.name}の給料が#{changed_employee.salary}に変わりました")
  end
end

class TaxMan
  def update(changed_employee)
    puts("#{changed_employee.name}に新しい請求書を送ります")
  end
end

class Employee
  attr_reader :name, :employee
  include Subject

  def initialize(name, title, salary)
    super()
    @name = name
    @title = title
    @salary = salary
  end

  def salary=(new_salary)
    @salary = new_salary
    notify_observers
  end
end

module Subject
  def initialize
    @observers = []
  end

  def add_observer(observer)
    @observers << observer
  end

  def delete_observer(observer)
    @observers.delete(observer)
  end

  def notify_observers
    @observers.each do |observer|
      observer.update(self)
    end
  end
end

payroll = Payroll.new
taxman = Taxman.new

taro = Employee.new('Taro', 'President', 200)

taro.add_observers(payroll)
taro.add_observers(taxman)

fred.salary = 300

このように、ニュースを持っているオブジェクトとそれを受け取る側にきれいなインターフェースを作るアイデアがObserverパターンです。ニュースを持っているオブジェクトはSubjectと呼ばれ、それに関心のあるオブジェクトはオブザーバーです。今の状態だと、Employeeはどれだけのオブザーバーが自分の給料の変更に関心があるかを知らなくて済みます。なので、新しくtaroの給料の変更を知る必要があるオブジェクトが出てきた場合は、そのオブジェクトがオブザーバーとしての共通のインターフェースであるupdateメソッドを実装して、add_observersするだけで、Employee自身は何も変化しません。

実例

こちらは実際に使われている例ではなく、rails-observersを使うと簡単にActiveRecord用のobserverクラスを作ることができるという説明をします。 githubのREADMEのサンプルコードですが、

class CommentObserver < ActiveRecord::Observer
  def after_save(comment)
    Notifications.comment("admin@do.com",
                          "New comment was posted",
                           comment).deliver
  end
end

この場合、Comment#saveが終了した際に、after_save内の処理を実行します。 サンプルコードでは、Subjectのセッターに、オブジェクトが変更した際に実行するメソッドを追加する必要がありましたが、rails-observersを使えば必要ありません。

Iteratorパターン

Iteratorパターンとは

Iteratorパターンとは集約オブジェクトが子オブジェクトのコレクションにアクセスする方法を外部に提供するテクニックです。 外部イテレーターと内部イテレータがあります。 外部イテレーターについて、サンプルコードを使って説明します。

class ArrayIteratoor
  def initialize(array)
    @array = array
    @index = 0
  end

  def has_next?
    @index < @array.length
  end

  def item
    @array[@index]
  end

  def next_item
    value = @array[@index]
    @index += 1
    value
  end
end

array = ['red', 'green', 'blue']
i = ArrayIterator.new(array)

while i.has_next?
  puts("item: #{i.next_item}")
end
=>item: red
item: green
item: blue

外部イテレーターの場合は上のように、子オブジェクトに対して処理内容を伝えます。 一方、内部イテレーターとは、eachのように、イテレーター自身がブロックを受け取って、 その処理内容を子オブジェクトに伝えます。

実例

内部イテレーターが使われている箇所を、railsソースコード内のactionpack/lib/action_dispatch/http/mime_type.rbから簡単に探すことができました。

module Mime
  class Mimes
    include Enumerable

    def initialize
      @mimes = []
      @symbols = nil
    end

    def each
      @mimes.each { |x| yield x }
    end

    def <<(type)
      @mimes << type
      @symbols = nil
    end

    def delete_if
      @mimes.delete_if { |x| yield x }.tap { @symbols = nil }
    end

    def symbols
      @symbols ||= map(&:to_sym)
    end
  end

  SET = Mimes.new

  class Type
  
  def register
  # 他の処理
  
  SET << new_mime
  # 他の処理
end

上の例の場合、Mimesクラスの子オブジェクトに対して処理内容を伝える場合は、コードブロックを利用します。 また、Enumberableモジュールをincludeしてeachメソッドを定義することで、include?, map, selectなどの配列を走査する際に便利なメソッドを使うことができます。 actionpack/lib/action_dispatch/http/mime_types.rbに下のような処理がありました。

Mime::Type.register "text/html", :html, %w( application/xhtml+xml ), %w( xhtml )
Mime::Type.register "text/plain", :text, [], %w(txt)
Mime::Type.register "text/javascript", :js, %w( application/javascript application/x-javascript )
Mime::Type.register "text/css", :css
Mime::Type.register "text/calendar", :ics
Mime::Type.register "text/csv", :csv
Mime::Type.register "text/vcard", :vcf
Mime::Type.register "text/vtt", :vtt, %w(vtt)

# 他のMimeタイプをMimeクラスに追加する処理が続く

Typeクラスのregisterメソッド内でMimesクラスに追加をする処理があり、actionpack/lib/action_dispatch/http/mime_types.rbでMimeタイプを追加していました。

Builderパターン

Builderパターンとは

オブジェクトの構築のロジックに対して責任をもつBuilderを作り、そのBuilderを使ってオブジェクトを作成するパターンです。 例としては、以下になります。

class Computer
  attr_reader :drives

 def initialize(drives=[])
    @drives = drives
  end
end

class Drive
  attr_reader :type
  attr_reader :size
  attr_reader :writable

  def initialize(type, size, writable)
    @type = type
    @size = size
    @writable = writable
  end
end

class ComputerBuilder
  attr_reader :computer
  
  def initialize
    @computer = Computer.new
  end

  def turbo(has_turbo_cpu = true)
    @couputer.motherboard_cpu = TurboCpu.new
  end

  def add_cd(writer = false)
    @computer.drives << Drive.new(:cd, 760, writer)
  end

  def add_dvd(writer = false)
    @computer.drives << Drive.new(:dvd, 4000, writer)
  end

  def add_hard_disk(size_in_mb)
    @computer.drives << Drive.new(:hard_disk, :size_in_bm, true)
  end
end

builder = ComputerBuilder.new
builder.add_cd(true)
builder.add_dvd
builder.add_hard_disk(1000000)

computer = builder.computer

上の場合は、Computerを作るために必要なcdのsizeが760であるというような、Computerクラスのインスタンスを作成するためのロジックが、ComputerBuilderのクラスに集まっています。Computerを作るための実装の詳細をBuilderクラスが隠蔽しています。

実例

BuilderパターンはBuilderでグレップすれば良いので、簡単に見つけることができました。 以下のソースコードは、mastodonという短文投稿型のSNSサイトのソースです。

github.com

# app/lib/rss_builder.rb

class RSSBuilder

  def initialize
    @document = Ox::Document.new(version: '1.0')
    @channel  = Ox::Element.new('channel')

    @document << (rss << @channel)
  end

  def title(str)
    @channel << (Ox::Element.new('title') << str)

    self
  end

  def link(str)
    @channel << (Ox::Element.new('link') << str)

    self
  end

 ## 他のメソッドが続く

RSSとは、Webサイトのニュースやブログなどの、更新情報の日付やタイトル、その内容の要約などを配信するため技術で、XML形式で記述されます。 このRSSBuilderは、titleがchannelの子要素であるなどのXMLの構成の詳細を隠蔽しています。 このRSSapp/serializers/rss/account_serializer.rb上でRSSBuilderクラスを使って作成されています。

class RSS::AccountSerializer

  def render(account, statuses, tag)
    builder.title("#タイトルの名前")
            .link("#タグのurl")
#他のRSSを作成する処理が続く

    builder.to_xml
  end

このAccountSerializerクラスはRSSxmlの構成の詳細を知りません。タイトルやlinkの情報を与えるだけでRSSを完成させることができます。

まとめ

Rubyによるデザインパターンを読んでみて、実際の使用例を探してみようと意気込んでみたものの中々見つけることができませんでした。 普段から、ソースコードを読む際にデザインパターンを意識して読んでみるのもアリなのかなと思いました。

また、実際に使用例を見つけるためのソースコードリーディングを通して、デザインパターンに対する理解を深めることができただけでなく、雑多に新たな知識を得ることができました。 そして、モヤモヤしていたことをはっきり理解できた時の快感がソースコードリーディングの楽しみだなと改めて感じたので、積極的に読んでいこうと思いました。

最後まで読んで頂き誠にありがとうございました。

参考

株式会社エニグモ 正社員の求人一覧

hrmos.co