Ruby 中的访问者模式,还是只使用块?

发布于 2024-08-06 10:29:23 字数 1287 浏览 2 评论 0原文

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

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

问题是,如果我想开始自定义 FormatVisitor 中的所有内容(假设它允许您指定节点的选项卡方式、属性的排序方式、属性的间距等),该怎么办?

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

我有几个选项:

  • 在构造函数中创建一个选项哈希 (FormatVisitor.new({:tabs => 2})
  • 在构造了 Visitor 后
  • 为每个新实现设置 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​​ 中读到的内容来看,这似乎有点矫枉过正。你怎么认为?无论哪种方式对我来说都很好,只是想知道你们对此有何感想。

多谢, 槊

Hey there, I have read the few posts here on when/how to use the visitor pattern, and some articles/chapters on it, and it makes sense if you are traversing an AST and it is highly structured, and you want to encapsulate the logic into a separate "visitor" object, etc. But with Ruby, it seems like overkill because you could just use blocks to do nearly the same thing.

I would like to pretty_print xml using Nokogiri. The author recommended that I use the visitor pattern, which would require I create a FormatVisitor or something similar, so I could just say "node.accept(FormatVisitor.new)".

The issue is, what if I want to start customizing all the stuff in the FormatVisitor (say it allows you to specify how nodes are tabbed, how attributes are sorted, how attributes are spaced, etc.).

  • One time I want the nodes to have 1 tab for each nest level, and the attributes to be in any order
  • The next time, I want the nodes to have 2 spaces, and the attributes in alphabetical order
  • The next time, I want them with 3 spaces and with two attributes per line.

I have a few options:

  • Create an options hash in the constructor (FormatVisitor.new({:tabs => 2})
  • Set values after I have constructed the Visitor
  • Subclass the FormatVisitor for each new implementation
  • Or just use blocks, not the visitor

Instead of having to construct a FormatVisitor, set values, and pass it to the node.accept method, why not just do this:


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

That's in contrast to what I feel like the visitor pattern would look like:


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

Maybe I've got the visitor pattern all wrong, but from what I've read about it in ruby, it seems like overkill. What do you think? Either way is fine with me, just wondering what how you guys feel about it.

Thanks a lot,
Lance

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

风向决定发型 2024-08-13 10:29:23

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

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

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

  • 对于多个相似但复杂的操作,访问者模式提供继承,而块则不提供。
  • 更干净地为 Visitor 类编写单独的测试套件。
  • 将较小的哑类合并为较大的智能类总是比将复杂的智能类分离为较小的哑类更容易。

您的实现似乎有点复杂,并且 Nokogiri 期望有一个实现 #visit 方法的 Visitor 实例,因此 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 使用工厂模式为特定节点实例化正确的格式化程序。在本例中,我区分了文本节点、叶元素节点、带文本的元素节点和常规元素节点。每种类型都有不同的格式要求。另请注意,这并不完整,例如未考虑注释节点。

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),用于控制属性的格式。

这演示了如何将几种模式组合在一起以获得您想要的灵活性。不过,如果您需要这些灵活性,则必须做出决定。

In essence, a Ruby block is the Visitor pattern without the extra boilerplate. For trivial cases, a block is sufficient.

For example, if you want to perform a simple operation on an Array object, you would just call the #each method with a block instead of implementing a separate Visitor class.

However, there are advantages in implementing a concrete Visitor pattern under certain cases:

  • For multiple, similar but complex operations, Visitor pattern provides inheritance and blocks don't.
  • Cleaner to write a separate test suite for Visitor class.
  • It's always easier to merge smaller, dumb classes into a larger smart class than separating a complex smart class into smaller dumb classes.

Your implementation seems mildly complex, and Nokogiri expects a Visitor instance that impelment #visit method, so Visitor pattern would actually be a good fit in your particular use case. Here is a class based implementation of the visitor pattern:

FormatVisitor implements the #visit method and uses Formatter subclasses to format each node depending on node types and other conditions.

# 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

Here the implementation of AttributesFormatter used above.

# 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 uses Factory pattern to instantiate the right formatter for a particular node. In this case I differentiated text node, leaf element node, element node with text, and regular element nodes. Each type has a different formatting requirement. Also note, that this is not complete, e.g. comment nodes are not taken into account.

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

To use this class:

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>

With the above code, you can change node formatting behaviors by constructing extra subclasses of NodeFromatters and plug them into the factory method. You can control the formatting of attributes with various implementation of the AttributesFromatter. As long as you adhere to its interface, you can plug it into the attributes_formatter_class argument without modifying anything else.

List of design patterns used:

  • Visitor Pattern: handle node traversal logic. ( Also interface requirement by Nokogiri. )
  • Factory Pattern, used to determine formatter based on node types and other formatting conditions. Note, if you don't like the class methods on NodeFormatter, you can extract them into NodeFormatterFactory to be more proper.
  • Dependency Injection (DI / IoC), used to control the formatting of attributes.

This demonstrates how you can combine a few patterns together to achieve the flexibility you desire. Although, if you need those flexibility is something you have to decide.

日记撕了你也走了 2024-08-13 10:29:23

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

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

I would go with what is simple and works. I don't know the details, but what you wrote compared with the Visitor pattern, looks simpler. If it also works for you, I would use that. Personally, I am tired with all these techniques that ask you to create a huge "network" of interelated classes, just to solve one small problem.

Some would say, yeah, but if you do it using patterns then you can cover many future needs and blah blah. I say, do now what works and if the need arises, you can refactor in the future. In my projects, that need almost never arises, but that's a different story.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文