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.
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. –
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. –
Muchas gracias, estos son todos los puntos importantes. –