2009-10-05 15 views
6

Ehilà, ho letto i pochi post qui su quando/come utilizzare il modello di visitatore, e alcuni articoli/capitoli su di esso, e ha senso se si sta attraversando un AST ed è altamente strutturato, e si desidera incapsula la logica in un oggetto "visitatore" separato, ecc. Ma con Ruby sembra eccessivo, perché potresti semplicemente usare i blocchi per fare quasi la stessa cosa.Visitor Pattern in Ruby o semplicemente usa un blocco?

Mi piacerebbe pretty_print xml utilizzando Nokogiri. L'autore mi ha raccomandato di utilizzare il pattern visitor, che richiede di creare un FormatVisitor o qualcosa di simile, quindi potrei semplicemente dire "node.accept (FormatVisitor.new)".

Il problema è, che cosa succede se voglio iniziare a personalizzare tutte le cose in FormatVisitor (diciamo che ti permette di specificare come i nodi sono tablati, come sono ordinati gli attributi, come sono distanziati gli attributi, ecc.).

  • Una volta che voglio i nodi di avere 1 scheda per ogni livello nido, e gli attributi per essere in qualsiasi ordine
  • La prossima volta, voglio i nodi di avere 2 spazi, e gli attributi in alfabetico ordine
  • La volta successiva, li voglio con 3 spazi e con due attributi per riga.

Ho un paio di opzioni:

  • Crea un hash opzioni nel costruttore (FormatVisitor.new ({: schede => 2})
  • Impostare i valori dopo aver costruito il Visitatore
  • sottoclasse il FormatVisitor per ogni nuova implementazione
  • o semplicemente usare i blocchi, non il visitatore

Invece di dover costruire una FormatVisitor, valori impostati, e passarlo al metodo node.accept, perché non basta fare questo:


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

Questo è in contrasto con quello che sento come il modello visitatore sarebbe guardare come:


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 

Forse ho il modello visitatore tutto sbagliato, ma da quello che ho letto su di esso in rubino, sembra eccessivo. Cosa ne pensi? Ad ogni modo va bene con me, mi chiedo solo cosa ne pensate voi ragazzi.

Grazie mille, Lance

risposta

7

vorrei andare con ciò che è semplice e funziona. Non conosco i dettagli, ma quello che hai scritto rispetto al pattern Visitor, sembra più semplice. Se funziona anche per te, lo userei. Personalmente, sono stanco di tutte queste tecniche che ti chiedono di creare un'enorme "rete" di classi interconnesse, solo per risolvere un piccolo problema.

Alcuni direbbero, sì, ma se lo fai usando schemi, puoi coprire molti bisogni futuri e bla bla. Dico, ora fa quello che funziona e se si presenta la necessità, puoi refactoring in futuro. Nei miei progetti, questo bisogno non emerge quasi mai, ma questa è una storia diversa.

+0

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. –

+6

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. –

+0

Grazie mille, sono tutti ottimi punti. –

12

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.

+0

Puoi aggiungere un buon esempio per descrivere il modello _visitor_. –

+0

Aggiunti esempi concreti, buon divertimento! –

+0

Grazie per questo. Ho un sacco di domande e concetti con modelli di design .. Se hai tempo, potresti aiutarmi a insegnare a quelli uno alla settimana ...? Ancora grazie per l'ulteriore spiegazione di questa risposta .. –

Problemi correlati