2009-10-05 20 views
6

Hola, he leído algunas publicaciones aquí sobre cuándo y cómo usar el patrón de visitante, y algunos artículos/capítulos sobre él, y tiene sentido si estás atravesando un AST y está altamente estructurado, y quieres encapsular la lógica en un objeto "visitante" separado, etc. Pero con Ruby, parece exagerado porque podrías usar bloques para hacer casi lo mismo.Patrón de visitante en Ruby, o simplemente usa un bloque?

Me gustaría pretty_print xml usando Nokogiri. El autor me recomendó que use el patrón de visitante, lo que requeriría que creara un FormatVisitor o algo similar, por lo que podría decir "node.accept (FormatVisitor.new)".

El problema es, ¿qué sucede si quiero comenzar a personalizar todo el material en el FormatVisitor (digamos que le permite especificar cómo se pescan los nudos, cómo se ordenan los atributos, cómo se espacian los atributos, etc.).

  • Una vez que quiere que los nodos tengan 1 ficha para cada nivel de la jerarquía, y los atributos para estar en cualquier orden
  • La próxima vez, quiero que los nodos que tienen 2 espacios, y los atributos en orden alfabético orden
  • La próxima vez, los quiero con 3 espacios y con dos atributos por línea.

que tienen algunas opciones:

  • Crear un hash de opciones en el constructor (FormatVisitor.new ({: pestañas => 2})
  • Valores de ajuste después de haber construido el visitante
  • Subclase el FormatVisitor para cada nueva aplicación
  • o simplemente utilizar los bloques, el visitante no

En lugar de tener que construir un FormatVisitor, valores de consigna, y pasarlo al método node.accept, ¿por qué no hacer esto:


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

Eso está en contraste con lo que me siento como el patrón de visitante se vería como:


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 

Tal vez tengo el patrón de visitante todo mal, pero por lo que he leído sobre él en rubí, que parece un exceso. ¿Qué piensas? De cualquier manera, está bien conmigo, solo me pregunto cómo se sienten ustedes al respecto.

Muchas gracias, lanza

Respuesta

7

Me gustaría ir con lo que es simple y funciona. No sé los detalles, pero lo que escribió en comparación con el patrón Visitor, parece más simple. Si también funciona para usted, lo usaría. Personalmente, estoy cansado de todas estas técnicas que te piden que crees una gran "red" de clases interrelacionadas, solo para resolver un pequeño problema.

Algunos dirían, sí, pero si lo haces usando patrones, entonces puedes cubrir muchas necesidades futuras y bla, bla. Digo, haga ahora lo que funciona y si surge la necesidad, puede refactorizar en el futuro. En mis proyectos, esa necesidad casi nunca surge, pero esa es una historia diferente.

+0

de acuerdo. Si necesita una mantenibilidad más fuerte, cree un método que pueda generar sus bloques, pero creo que el patrón de visitantes se puede reconstruir utilizando el código Ruby nativo, al igual que el patrón Factory es prácticamente realizable mediante el uso de inicializadores nativos. –

+6

En realidad, el beneficio real de los patrones es que ayudan al mantenimiento facilitando que otros entiendan lo que está tratando de lograr con el patrón. Con demasiada frecuencia, la intención se pierde en la implementación: si reconoce el patrón, tiene más posibilidades de reconocer el intento. –

+0

Muchas gracias, estos son todos los puntos importantes. –

12

En esencia, un bloque de Ruby es el patrón Visitor sin el texto adicional. Para casos triviales, un bloqueo es suficiente.

Por ejemplo, si desea realizar una operación simple en un objeto Array, simplemente llame al método #each con un bloque en lugar de implementar una clase Visitor por separado.

Sin embargo, hay ventajas en la aplicación de un patrón de Visitantes de hormigón en ciertos casos:

  • Para múltiples, complejas operaciones similares pero, patrón de Visitantes ofrece la herencia y bloques no lo hacen.
  • Cleaner para escribir un conjunto de pruebas separado para la clase de visitante.
  • Siempre es más fácil combinar clases más pequeñas y tontas en una clase inteligente más grande que separar una clase inteligente compleja en clases más pequeñas y tontas.

Su aplicación parece estar levemente complejo y Nokogiri espera una instancia de visitantes que impelment #visit método, por lo que el patrón del visitante en realidad sería un buen ajuste en su caso en particular. Aquí hay una implementación basada en la clase del patrón de visitante:

FormatVisitor implementa el método #visit y usa las subclases Formatter para formatear cada nodo según los tipos de nodos y otras condiciones.

# 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 

Aquí la implementación de AttributesFormatter utilizado anteriormente.

# 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 utiliza patrón de fábrica para instanciar el formateador derecho para un nodo particular. En este caso, diferencié nodo de texto, nodo de elemento hoja, nodo elemento con texto y nodos de elementos regulares. Cada tipo tiene un requisito de formato diferente. También tenga en cuenta que esto no está completo, p. los nodos de comentarios no se tienen en cuenta.

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 

Para utilizar esta clase:

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> 

Con el código anterior, puede cambiar los comportamientos de formato nodo mediante la construcción de las subclases adicionales de NodeFromatter s y enchufarlos en el método de fábrica. Puede controlar el formateo de atributos con varias implementaciones del AttributesFromatter. Siempre que se adhiera a su interfaz, puede conectarlo al argumento attributes_formatter_class sin modificar nada más.

Lista de patrones de diseño utilizados:

  • Patrón Visitante: manejar la lógica de recorrido de nodos. (También es requisito de interfaz de Nokogiri).
  • Factory Pattern, que se utiliza para determinar el formateador según los tipos de nodos y otras condiciones de formateo. Tenga en cuenta que si no le gustan los métodos de clase en NodeFormatter, puede extraerlos en NodeFormatterFactory para que sean más adecuados.
  • Dependencia de inyección (DI/IoC), que se utiliza para controlar el formateo de los atributos.

Esto demuestra cómo puede combinar unos patrones para lograr la flexibilidad que desea. Aunque, si necesita esa flexibilidad es algo que tiene que decidir.

+0

¿Puede agregar un buen ejemplo para describir el patrón _visitor_. –

+0

Agregó algunos ejemplos concretos, ¡disfrútelo! –

+0

Gracias por esto. Tengo muchas preguntas y concepciones con patrones de diseño ... Si tienes tiempo, ¿podrías ayudarme a enseñar esos por semana ...? De nuevo, gracias por la explicación adicional de esta respuesta. –

Cuestiones relacionadas