こんにちは、サーバーサイドエンジニアの平井です。
こちらは、Enigmo Advent Calendar 2019 、24日目の記事です。
昨年の1月にエニグモにインターンとして入社してから一年が経とうとしています。早いもので、新卒の肩書きもそろそろ無くなってしまいますね。
今回は、Rubyによるデザインパターンを読んで、デザインパターンを勉強したので、そのアウトプットをさせていただきます。
タイトルの通り、デザインパターンについて実際の使用例を探してみました。そのパターンと使用例は以下になります。
- Strategyパターン
- Observerパターン
- Iteratorパターン
- Builderパターン
まずは、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パターンとは集約オブジェクトが子オブジェクトのコレクションにアクセスする方法を外部に提供するテクニックです。
外部イテレーターと内部イテレータがあります。
外部イテレーターについて、サンプルコードを使って説明します。
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)
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
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の構成の詳細を隠蔽しています。
このRSSはapp/serializers/rss/account_serializer.rb上でRSSBuilderクラスを使って作成されています。
class RSS::AccountSerializer
def render(account, statuses, tag)
builder.title("#タイトルの名前")
.link("#タグのurl")
builder.to_xml
end
このAccountSerializerクラスはRSSのxmlの構成の詳細を知りません。タイトルやlinkの情報を与えるだけでRSSを完成させることができます。
まとめ
Rubyによるデザインパターンを読んでみて、実際の使用例を探してみようと意気込んでみたものの中々見つけることができませんでした。
普段から、ソースコードを読む際にデザインパターンを意識して読んでみるのもアリなのかなと思いました。
また、実際に使用例を見つけるためのソースコードリーディングを通して、デザインパターンに対する理解を深めることができただけでなく、雑多に新たな知識を得ることができました。
そして、モヤモヤしていたことをはっきり理解できた時の快感がソースコードリーディングの楽しみだなと改めて感じたので、積極的に読んでいこうと思いました。
最後まで読んで頂き誠にありがとうございました。
参考
株式会社エニグモ 正社員の求人一覧
hrmos.co