實質上,一個Ruby塊是訪客模式沒有額外的樣板。對於微不足道的情況,一個塊就足夠了。
例如,如果要對Array對象執行簡單操作,則只需使用塊調用#each
方法,而不是實現單獨的Visitor類。
不過,也有在執行在某些情況下,具體的Visitor模式的優點:
- 對於多個,類似,但複雜的操作,訪問者模式提供了繼承和塊沒有。
- 清理者爲Visitor類編寫單獨的測試套件。
- 將較小的笨類合併到較大的智能類中比將複雜的智能類分成較小的笨類要容易得多。
你的實現似乎婉轉複雜,並引入nokogiri預計impelment #visit
方法,所以Visitor模式實際上是一個不錯的選擇在您的特定用例訪問者實例。這裏是訪問者模式的一類基於實現:
FormatVisitor實現#visit
方法和使用Formatter
子類取決於節點類型和其它條件來格式化每個節點。
# FormatVisitor implments the #visit method and uses formatter to format
# each node recursively.
class FormatVistor
attr_reader :io
# Set some initial conditions here.
# Notice that you can specify a class to format attributes here.
def initialize(io, tab: " ", depth: 0, attributes_formatter_class: AttributesFormatter)
@io = io
@tab = tab
@depth = depth
@attributes_formatter_class = attributes_formatter_class
end
# Visitor interface. This is called by Nokogiri node when Node#accept
# is invoked.
def visit(node)
NodeFormatter.format(node, @attributes_formatter_class, self)
end
# helper method to return a string with tabs calculated according to depth
def tabs
@tab * @depth
end
# creates and returns another visitor when going deeper in the AST
def descend
self.class.new(@io, {
tab: @tab,
depth: @depth + 1,
attributes_formatter_class: @attributes_formatter_class
})
end
end
這裏執行AttributesFormatter
上面使用。
# This is a very simple attribute formatter that writes all attributes
# in one line in alphabetical order. It's easy to create another formatter
# with the same #initialize and #format interface, and you can then
# change the logic however you want.
class AttributesFormatter
attr_reader :attributes, :io
def initialize(attributes, io)
@attributes, @io = attributes, io
end
def format
return if attributes.empty?
sorted_attribute_keys.each do |key|
io << ' ' << key << '="' << attributes[key] << '"'
end
end
private
def sorted_attribute_keys
attributes.keys.sort
end
end
NodeFormatter
S使用工廠模式來實例化權格式化爲特定節點。在這種情況下,我區分了文本節點,葉元素節點,具有文本的元素節點和常規元素節點。每種類型都有不同的格式要求。還要注意,這並不完整,例如評論節點不被考慮在內。
class NodeFormatter
# convience method to create a formatter using #formatter_for
# factory method, and calls #format to do the formatting.
def self.format(node, attributes_formatter_class, visitor)
formatter_for(node, attributes_formatter_class, visitor).format
end
# This is the factory that creates different formatters
# and use it to format the node
def self.formatter_for(node, attributes_formatter_class, visitor)
formatter_class_for(node).new(node, attributes_formatter_class, visitor)
end
def self.formatter_class_for(node)
case
when text?(node)
Text
when leaf_element?(node)
LeafElement
when element_with_text?(node)
ElementWithText
else
Element
end
end
# Is the node a text node? In Nokogiri a text node contains plain text
def self.text?(node)
node.class == Nokogiri::XML::Text
end
# Is this node an Element node? In Nokogiri an element node is a node
# with a tag, e.g. <img src="foo.png" /> It can also contain a number
# of child nodes
def self.element?(node)
node.class == Nokogiri::XML::Element
end
# Is this node a leaf element node? e.g. <img src="foo.png" />
# Leaf element nodes should be formatted in one line.
def self.leaf_element?(node)
element?(node) && node.children.size == 0
end
# Is this node an element node with a single child as a text node.
# e.g. <p>foobar</p>. We will format this in one line.
def self.element_with_text?(node)
element?(node) && node.children.size == 1 && text?(node.children.first)
end
attr_reader :node, :attributes_formatter_class, :visitor
def initialize(node, attributes_formatter_class, visitor)
@node = node
@visitor = visitor
@attributes_formatter_class = attributes_formatter_class
end
protected
def attribute_formatter
@attribute_formatter ||= @attributes_formatter_class.new(node.attributes, io)
end
def tabs
visitor.tabs
end
def io
visitor.io
end
def leaf?
node.children.empty?
end
def write_tabs
io << tabs
end
def write_children
v = visitor.descend
node.children.each { |child| child.accept(v) }
end
def write_attributes
attribute_formatter.format
end
def write_open_tag
io << '<' << node.name
write_attributes
if leaf?
io << '/>'
else
io << '>'
end
end
def write_close_tag
return if leaf?
io << '</' << node.name << '>'
end
def write_eol
io << "\n"
end
class Element < self
def format
write_tabs
write_open_tag
write_eol
write_children
write_tabs
write_close_tag
write_eol
end
end
class LeafElement < self
def format
write_tabs
write_open_tag
write_eol
end
end
class ElementWithText < self
def format
write_tabs
write_open_tag
io << text
write_close_tag
write_eol
end
private
def text
node.children.first.text
end
end
class Text < self
def format
write_tabs
io << node.text
write_eol
end
end
end
要使用這個類:
xml = "<root><aliens><alien><name foo=\"bar\">Alf<asdf/></name></alien></aliens></root>"
doc = Nokogiri::XML(xml)
# the FormatVisitor accepts an IO object and writes to it
# as it visits each node, in this case, I pick STDOUT.
# You can also use File IO, Network IO, StringIO, etc.
# As long as it support the #puts method, it will work.
# I'm using the defaults here. (two spaces, with starting depth at 0)
visitor = FormatVisitor.new(STDOUT)
# this will allow doc (the root node) to call visitor.visit with
# itself. This triggers the visiting of each children recursively
# and contents written to the IO object. (In this case, it will
# print to STDOUT.
doc.accept(visitor)
# Prints:
# <root>
# <aliens>
# <alien>
# <name foo="bar">
# Alf
# <asdf/>
# </name>
# </alien>
# </aliens>
# </root>
與上面的代碼,你可以通過構建的NodeFromatter
小號額外的子類改變節點格式的行爲,並將其插入工廠方法。您可以通過AttributesFromatter
的各種實施控制屬性的格式。只要你堅持它的界面,你可以將其插入attributes_formatter_class
參數,而無需修改任何東西。
的設計模式一覽表中所用:
- 訪問者模式:處理節點遍歷的邏輯。 (另外,通過引入nokogiri接口要求。)
- 工廠模式,使用了基於節點類型和其他格式的條件,以確定格式。請注意,如果您不喜歡
NodeFormatter
上的類方法,則可以將它們解壓縮爲NodeFormatterFactory
以更加合適。
- 依賴注入(DI/IOC),用於控制屬性的格式。
這演示瞭如何將幾個圖案組合在一起以實現您想要的靈活性。雖然,如果你需要那些靈活性是你必須決定。
同意。如果您需要更強大的可維護性,請創建一個可以生成塊的方法,但我覺得訪問者模式可以使用本機Ruby代碼進行重構,就像Factory模式使用本機初始化程序幾乎可以構建一樣。 –
實際上,模式的真正好處在於,他們通過使其他人瞭解您嘗試使用模式實現的內容,從而幫助維護。在執行過程中往往意圖喪失 - 如果你認識到這種模式,你就有更好的機會認識到這個意圖。 –
非常感謝,這些都是很棒的一點。 –