Tbpgr Blog

Employee Experience Engineer tbpgr(てぃーびー) のブログ

RubyでVisitorパターン

概要

GoFデザインパターンのVisitorパターンについて。
データ構造と処理の分離を行うことによって
OPC(The Open-Closed Principle)を実現する。
拡張については開かれていて、修正については閉じられている。

VisitorとElementが協調して処理を行う形式を
ダブルディスパッチという。
A,Bがセットで処理を決定する。

登場人物

Visitor = 抽象ビジター。訪問者
ConcreteVisitor = 具象ビジター。訪問者
Element = 受入要素のインターフェース
ConcreteElement = 具象要素
ConcreteStructure = 集合体

UML


実装サンプル

サンプル概要

Compositパターンで使用したWebサイトのリンク構成を実装したクラスを流用。
Webサイト全体はWebPageクラス。リンクを保持するページとリンクを保持しないページがあります。
リンクを保持するページはLinkPageクラス。
リンクを保持しない終端サイトはTerminatePage。
各WebPageを訪問するのがVisitor役のFormatterクラスを実装したCsvFormatter,HatenaFormatterとします。

CsvFormatterはサイトを

サイト名,URL

で表示します。

HatenaFormatterはサイトを

[URL;title=サイト名]

で表示します。

登場人物

Element = Accepter : インターフェース
Component = WebPage : コンポーネント。ウェブページ全般
ConcreteElement = LinkPage : コンポジット。リンクページ。ConcreteStructureも兼ねる
ConcreteElement = TerminatePage : 葉。再帰しない要素。終端ページ
Visitor = Formatter : 整形
ConcreteVisitor = CsvFormatter : CSV整形
ConcreteVisitor = HatenaFormatter : CSV整形

UML


サンプルコード

formatter

# encoding: Shift_JIS
require_relative './accepter'

=begin rdoc
= Formatterクラス
=end
class Formatter
  NOT_OVERRRIDE = 'not override error'
  def format(accepter)
    raise NOT_OVERRRIDE
  end
end

csv_formatter

# encoding: Shift_JIS
require_relative './formatter'

=begin rdoc
= CsvFormatterクラス
=end
class CsvFormatter < Formatter
  def format(accepter)
    output = ""
    output << "#{accepter.title},#{accepter.url}\n"
    return output if accepter.get_children.nil?
    
    children = accepter.get_children
    children.each{|child|
      output << child.accept(self)
    }
    return output
  end
end

hatena_formatter

# encoding: Shift_JIS
require_relative './formatter'

=begin rdoc
= HatenaFormatterクラス
=end
class HatenaFormatter < Formatter
  def format(accepter)
    output = ""
    output << "[#{accepter.url};title=#{accepter.title}]\n"
    return output if accepter.get_children.nil?
    
    children = accepter.get_children
    children.each{|child|
      output << child.accept(self)
    }
    return output
  end
end

accepter

# encoding: Shift_JIS
require_relative './formatter'

=begin rdoc
= Accepterクラス
=end
class Accepter
  NOT_OVERRRIDE = 'not override error'
  def accept(formatter)
    formatter.format(self)
  end
end

web_page

# encoding: Shift_JIS
require_relative './accepter'

=begin rdoc
= WebPageクラス
=end
class WebPage < Accepter
  attr_accessor :title,:url,:web_page_children
  NOT_OVERRRIDE = 'not override error'
  def initialize()
    @web_page_children = Array.new
  end

  def add(web_page)
    raise WebPage::NOT_OVERRRIDE
  end
  
  def remove(remove_page)
    raise WebPage::NOT_OVERRRIDE
  end

  def get_children()
    raise WebPage::NOT_OVERRRIDE
  end
end

link_page

# encoding: Shift_JIS
require_relative './web_page'

=begin rdoc
= WebPageクラス
=end
class LinkPage < WebPage
  def add(web_page)
    @web_page_children.push web_page
  end
  
  def remove(remove_page)
    web_page.delete remove_page.title
  end

  def get_children()
    return web_page_children
  end
end

terminate_page

# encoding: Shift_JIS
require_relative './web_page'

=begin rdoc
= TerminatePage クラス
=end
class TerminatePage < WebPage
  def get_children()
    return nil
  end
end

main

# encoding: Shift_JIS
require_relative './accepter'
require_relative './web_page'
require_relative './terminate_page'
require_relative './link_page'
require_relative './csv_formatter'
require_relative './hatena_formatter'

root_page = LinkPage.new
root_page.title = 'ルートページ'
root_page.url = 'http://ルートページ.com'

link_page1 = LinkPage.new
link_page1.title = 'リンクページ1'
link_page1.url = 'http://リンクページ1.com'

terminate_page1 = TerminatePage.new
terminate_page1.title = "末端タイトル1"
terminate_page1.url = 'http://末端1.com'

terminate_page1_1 = TerminatePage.new
terminate_page1_1.title = "末端タイトル1_1"
terminate_page1_1.url = 'http://末端1_1.com'

link_page2 = LinkPage.new
link_page2.title = 'リンクページ2'
link_page2.url = 'http://リンクページ2.com'


link_page1.add(terminate_page1_1)
root_page.add(link_page1)
root_page.add(terminate_page1)
root_page.add(link_page2)

puts "−−−−CSVフォーマット−−−−"
csv_formatter = CsvFormatter.new
puts csv_formatter.format root_page

puts "−−−−Hatenaフォーマット−−−−"
hatena_formatter = HatenaFormatter.new
puts hatena_formatter.format root_page
出力結果
−−−−CSVフォーマット−−−−
ルートページ,http://ルートページ.com
リンクページ1,http://リンクページ1.com
末端タイトル1_1,http://末端1_1.com
末端タイトル1,http://末端1.com
リンクページ2,http://リンクページ2.com
−−−−Hatenaフォーマット−−−−
[http://ルートページ.com;title=ルートページ]
[http://リンクページ1.com;title=リンクページ1]
[http://末端1_1.com;title=末端タイトル1_1]
[http://末端1.com;title=末端タイトル1]
[http://リンクページ2.com;title=リンクページ2]