In sostanza, un blocco Ruby è il pattern Visitor senza la piastra aggiuntiva. Per casi banali, un blocco è sufficiente.
Ad esempio, se si desidera eseguire un'operazione semplice su un oggetto Array, è sufficiente chiamare il metodo #each
invece di implementare una classe Visitor separata.
Tuttavia, vi sono vantaggi nell'applicazione di modello Visitor cemento sotto taluni casi:
- Per operazioni multiple, simili ma complessi, modello Visitor fornisce eredità e blocchi non.
- Pulitore per scrivere una suite di test separata per la classe Visitor.
- È sempre più facile unire classi più piccole e stupide in una classe intelligente più ampia della separazione di una classe smart complessa in classi più piccole.
L'implementazione sembra leggermente complessa, e si aspetta Nokogiri un'istanza visitatore che impelment #visit
metodo, così modello Visitatore sarebbe in realtà una buona misura nel vostro particolare caso d'uso. Ecco un'implementazione basata sulla classe del modello visitatore:
FormatVisitor implementa il metodo #visit
e utilizza le sottoclassi Formatter
per formattare ciascun nodo in base ai tipi di nodo e ad altre condizioni.
# 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
Qui l'implementazione di AttributesFormatter
utilizzata in precedenza.
# 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 utilizza pattern Factory per creare un'istanza il formattatore giusta per un nodo particolare. In questo caso ho distinto il nodo del testo, il nodo dell'elemento foglia, il nodo dell'elemento con il testo e i nodi degli elementi regolari. Ogni tipo ha un diverso requisito di formattazione. Inoltre, nota che questo non è completo, ad es. i nodi di commento non vengono presi in considerazione.
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
Per utilizzare questa classe:
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 il codice di cui sopra, è possibile modificare i comportamenti di formattazione nodo costruendo sottoclassi extra di NodeFromatter
s e inserirli nel metodo di fabbrica. È possibile controllare la formattazione degli attributi con varie implementazioni di AttributesFromatter
. Finché si aderisce alla sua interfaccia, è possibile collegarlo all'argomento attributes_formatter_class
senza modificare altro.
Elenco dei modelli di progettazione utilizzata:
- pattern Visitor: gestire logica attraversamento nodo. (Necessario anche l'interfaccia richiesta da Nokogiri.)
- Modello di fabbrica, utilizzato per determinare il formattatore in base ai tipi di nodo e ad altre condizioni di formattazione. Nota, se non ti piacciono i metodi di classe su
NodeFormatter
, puoi estrapolarli in NodeFormatterFactory
per essere più adatti.
- Iniezione di dipendenza (DI/IoC), utilizzato per controllare la formattazione degli attributi.
Questo dimostra come è possibile combinare alcuni modelli per ottenere la flessibilità desiderata. Anche se, se hai bisogno di quella flessibilità è qualcosa devi decidere.
concordato. Se hai bisogno di maggiore manutenibilità, crea un metodo in grado di generare i tuoi blocchi, ma ritengo che il pattern del visitatore possa essere ricostruito usando il codice nativo di Ruby, proprio come il pattern Factory è abbastanza costruibile usando gli inizializzatori nativi. –
In realtà il vero vantaggio dei pattern è che aiutano la manutenzione rendendo facile per gli altri capire cosa si sta cercando di ottenere con lo schema. Troppo spesso l'intenzione si perde nell'implementazione - se riconosci il modello, hai q una migliore possibilità di riconoscere l'intento. –
Grazie mille, sono tutti ottimi punti. –