嘿,我已经阅读了这里关于何时/如何使用访问者模式的几篇文章,以及一些关于它的文章/章节,如果你正在遍历一个 AST 并且它是高度结构化的,并且你想要将逻辑封装到单独的“访问者”对象等中。但是对于 Ruby,这似乎有点过分,因为您可以只使用 block 来完成几乎相同的事情。
我想使用 Nokogiri 漂亮地打印 xml。作者建议我使用访问者模式,这需要我创建一个 FormatVisitor 或类似的东西,所以我可以只说“node.accept(FormatVisitor.new)”。
问题是,如果我想开始自定义 FormatVisitor 中的所有内容怎么办(假设它允许您指定节点的选项卡方式、属性的排序方式、属性的间隔方式等)。
- 有一次我希望节点的每个嵌套级别都有 1 个选项卡,并且属性可以按任意顺序
- 下次我要节点有2个空格,属性按字母顺序排列
- 下一次,我希望它们有 3 个空格并且每行有两个属性。
我有几个选择:
- 在构造函数中创建一个选项散列 (FormatVisitor.new({:tabs => 2})
- 在构建访问者后设置值
- 为每个新实现子类化 FormatVisitor
- 或者只使用 block ,而不是访问者
与其构建 FormatVisitor、设置值并将其传递给 node.accept 方法,不如这样做:
<code>node.pretty_print do |format|
format.tabs = 2
format.sort_attributes_by {...}
end</code>
这与我认为的访客模式形成对比:
<code>visitor = Class.new(FormatVisitor) do
attr_accessor :format
def pretty_print(node)
# do something with the text
@format.tabs = 2 # two tabs per nest level
@format.sort_attributes_by {...}
end
end.new
doc.children.each do |child|
child.accept(visitor)
end</code>
也许我完全弄错了访问者模式,但从我在 ruby 中读到的内容来看,这似乎有些矫枉过正。你怎么认为?无论哪种方式对我来说都很好,只是想知道你们对此有何看法。
非常感谢, 兰斯
最佳答案
本质上,Ruby block 是没有额外样板的访问者模式。对于微不足道的情况,一个 block 就足够了。
例如,如果您想对 Array 对象执行简单的操作,您只需使用 block 调用 #each
方法,而不是实现单独的 Visitor 类。
但是,在某些情况下实现具体的访问者模式有一些优势:
- 对于多个相似但复杂的操作,访问者模式提供继承而 block 不提供。
- 能够更清楚地为 Visitor 类编写单独的测试套件。
- 将较小的哑类合并成较大的智能类总是比将复杂的智能类分成较小的哑类更容易。
你的实现看起来有点复杂,Nokogiri 期望一个 Visitor 实例来插入 #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
的各种实现来控制属性的格式。只要您遵守其接口(interface),就可以将其插入 attributes_formatter_class
参数,而无需修改任何其他内容。
使用的设计模式列表:
- 访问者模式:处理节点遍历逻辑。 (也是 Nokogiri 的接口(interface)要求。)
- 工厂模式,用于根据节点类型和其他格式化条件确定格式化程序。请注意,如果您不喜欢
NodeFormatter
上的类方法,您可以将它们提取到NodeFormatterFactory
中以更合适。 - 依赖注入(inject) (DI/IoC),用于控制属性的格式。
这演示了如何将一些模式组合在一起以实现所需的灵 active 。不过,如果您需要这些灵 active ,则您必须做出决定。
关于ruby - Ruby 中的访问者模式,还是只使用 block ?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/1518474/