こんにちは、サーバーサイドエンジニアの平井です。
こちらは、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という認証フレームワークで使われています。
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サイトのソースです。
# 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の構成の詳細を隠蔽しています。 このRSSはapp/serializers/rss/account_serializer.rb上でRSSBuilderクラスを使って作成されています。
class RSS::AccountSerializer def render(account, statuses, tag) builder.title("#タイトルの名前") .link("#タグのurl") #他のRSSを作成する処理が続く builder.to_xml end
このAccountSerializerクラスはRSSのxmlの構成の詳細を知りません。タイトルやlinkの情報を与えるだけでRSSを完成させることができます。
まとめ
Rubyによるデザインパターンを読んでみて、実際の使用例を探してみようと意気込んでみたものの中々見つけることができませんでした。 普段から、ソースコードを読む際にデザインパターンを意識して読んでみるのもアリなのかなと思いました。
また、実際に使用例を見つけるためのソースコードリーディングを通して、デザインパターンに対する理解を深めることができただけでなく、雑多に新たな知識を得ることができました。 そして、モヤモヤしていたことをはっきり理解できた時の快感がソースコードリーディングの楽しみだなと改めて感じたので、積極的に読んでいこうと思いました。
最後まで読んで頂き誠にありがとうございました。
参考
- Design Patterns in Ruby: Strategy Pattern - Ruby Inside - Medium
- GitHub - rails/rails-observers: Rails observer (removed from core in Rails 4.0)
- GitHub - rails/rails: Ruby on Rails
- https://github.com/tootsuite/mastodon
株式会社エニグモ 正社員の求人一覧