Шаблон посетителя в Ruby или просто использовать блок? - PullRequest
7 голосов
/ 05 октября 2009

Привет, я прочитал несколько постов здесь о том, когда / как использовать шаблон посетителя, и некоторые статьи / главы о нем, и имеет смысл, если вы пересекаете AST, и он очень структурирован, и вы хотите инкапсулировать логику в отдельный объект "посетитель" и т. д. Но с Ruby это кажется излишним, потому что вы можете просто использовать блоки, чтобы сделать почти то же самое.

Я бы хотел, чтобы pretty_print xml использовал Nokogiri. Автор рекомендовал мне использовать шаблон посетителя, который потребовал бы создания FormatVisitor или чего-то подобного, чтобы я мог просто сказать «node.accept (FormatVisitor.new)».

Проблема в том, что если я захочу начать настройку всего содержимого в FormatVisitor (скажем, он позволяет вам указать, как узлы вкладываются, как атрибуты сортируются, как распределяются атрибуты и т. Д.).

  • Однажды я хочу, чтобы у узлов была 1 вкладка для каждого уровня гнезда, а атрибуты располагались в любом порядке
  • В следующий раз я хочу, чтобы узлы имели 2 пробела и атрибуты в алфавитном порядке
  • В следующий раз я хочу, чтобы они были с 3 пробелами и двумя атрибутами в каждой строке.

У меня есть несколько вариантов:

  • Создать хеш параметров в конструкторе (FormatVisitor.new ({: tabs => 2})
  • Установить значения после того, как я построил посетителя
  • Подкласс FormatVisitor для каждой новой реализации
  • Или просто используйте блоки, а не посетителя

Вместо того, чтобы создавать 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, это похоже на излишество. Как вы думаете? В любом случае, со мной все в порядке, просто интересно, что вы, ребята, чувствуете по этому поводу.

Большое спасибо, Lance

Ответы [ 2 ]

13 голосов
/ 22 марта 2012

По сути, блок Ruby является шаблоном Visitor без дополнительного шаблона. Для тривиальных случаев достаточно блока.

Например, если вы хотите выполнить простую операцию над объектом Array, вы просто вызовете метод #each с блоком вместо реализации отдельного класса Visitor.

Тем не менее, есть определенные преимущества в реализации конкретного шаблона Visitor в определенных случаях:

  • Для нескольких похожих, но сложных операций шаблон Visitor обеспечивает наследование, а блоки - нет.
  • Очиститель для написания отдельного набора тестов для класса 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 использует шаблон Factory для создания правильного модуля форматирования для определенного узла. В этом случае я различал текстовый узел, листовой узел элемента, узел элемента с текстом и узлы обычного элемента. Каждый тип имеет свои требования к форматированию. Также обратите внимание, что это не завершено, например, узлы комментариев не учитываются.

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), используется для управления форматированием атрибутов.

Это демонстрирует, как вы можете объединить несколько шаблонов вместе для достижения желаемой гибкости. Хотя, если вам нужно , эта гибкость - это что-то , вам нужно решить.

7 голосов
/ 05 октября 2009

Я бы пошел с тем, что просто и работает. Я не знаю деталей, но то, что вы написали по сравнению с шаблоном Visitor, выглядит проще. Если это также работает для вас, я бы использовал это. Лично я устал от всех этих техник, которые просят вас создать огромную «сеть» взаимосвязанных классов, просто чтобы решить одну маленькую проблему.

Кто-то скажет, да, но если вы сделаете это с помощью шаблонов, вы сможете покрыть многие будущие потребности и все такое. Я говорю: делай сейчас то, что работает, и если возникнет такая необходимость, ты сможешь провести рефакторинг в будущем. В моих проектах это почти никогда не возникает, но это другая история.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...