11

嘿,我已经阅读了关于何时/如何使用访问者模式的几篇文章,以及有关它的一些文章/章节,如果您正在遍历 AST 并且它是高度结构化的,并且您想要封装逻辑到一个单独的“访问者”对象等。但是对于 Ruby,这似乎有点矫枉过正,因为你可以使用块来做几乎相同的事情。

我想使用 Nokogiri 打印漂亮的 xml。作者建议我使用访问者模式,这需要我创建一个 FormatVisitor 或类似的东西,所以我可以说“node.accept(FormatVisitor.new)”。

问题是,如果我想开始自定义 FormatVisitor 中的所有内容怎么办(比如它允许您指定节点的标签方式、属性的排序方式、属性的间隔方式等)。

  • 有一次我希望节点为每个嵌套级别有 1 个选项卡,并且属性按任意顺序排列
  • 下一次,我希望节点有 2 个空格,并且属性按字母顺序排列
  • 下一次,我希望它们有 3 个空格,每行有两个属性。

我有几个选择:

  • 在构造函数中创建一个选项哈希 (FormatVisitor.new({:tabs => 2})
  • 在我构建了访问者之后设置值
  • 为每个新实现子类化 FormatVisitor
  • 或者只使用块,而不是访问者

不必构造 FormatVisitor、设置值并将其传递给 node.accept 方法,为什么不这样做:


node.pretty_print do |format|
  format.tabs = 2
  format.sort_attributes_by {...}
end

这与我认为访问者模式的样子形成鲜明对比:


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

也许我把访问者模式弄错了,但从我在 ruby​​ 中读到的关于它的内容来看,这似乎有点矫枉过正。你怎么看?无论哪种方式,我都很好,只是想知道你们对此有何感受。

非常感谢,兰斯

4

2 回答 2

17

本质上,Ruby 块没有额外样板的访问者模式。对于琐碎的情况,一个块就足够了。

例如,如果您想对 Array 对象执行简单的操作,您只需#each使用块调用该方法,而不是实现单独的 Visitor 类。

然而,在某些情况下实现一个具体的访问者模式是有优势的:

  • 对于多个相似但复杂的操作,访问者模式提供继承而块不提供。
  • 为访客类编写单独的测试套件的清洁工。
  • 将更小的哑类合并到更大的智能类中总是比将复杂的智能类分成更小的哑类更容易。

您的实现似乎有点复杂,并且 Nokogiri 需要一个执行#visit方法的访问者实例,因此访问者模式实际上非常适合您的特定用例。这是访问者模式的基于类的实现:

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

NodeFormatters 使用工厂模式为特定节点实例化正确的格式化程序。在这种情况下,我区分了文本节点、叶元素节点、带有文本的元素节点和常规元素节点。每种类型都有不同的格式要求。另请注意,这并不完整,例如未考虑评论节点。

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使用上面的代码,您可以通过构造额外的s 子类并将它们插入工厂方法来更改节点格式化行为。您可以使用AttributesFromatter. 只要您遵守它的接口,您就可以将其插入attributes_formatter_class参数中而无需修改任何其他内容。

使用的设计模式列表:

  • 访问者模式:处理节点遍历逻辑。(也是 Nokogiri 的接口要求。)
  • 工厂模式,用于根据节点类型和其他格式化条件确定格式化程序。请注意,如果您不喜欢 上的类方法NodeFormatter,可以将它们提取到NodeFormatterFactory更合适的位置。
  • 依赖注入(DI/IoC),用于控制属性的格式化。

这演示了如何将几个模式组合在一起以实现所需的灵活性。虽然,如果你需要那些灵活性是必须决定的。

于 2012-03-22T18:56:57.550 回答
8

我会选择简单且有效的方法。我不知道细节,但是与访问者模式相比,您编写的内容看起来更简单。如果它也适合你,我会使用它。就个人而言,我厌倦了所有这些要求你创建一个巨大的相互关联的类“网络”来解决一个小问题的技术。

有人会说,是的,但如果你使用模式来做,那么你可以满足许多未来的需求,等等。我说,现在做有效的事,如果需要,你可以在未来重构。在我的项目中,这种需求几乎从未出现,但那是另一回事。

于 2009-10-05T06:35:01.777 回答