11

ねえ、私はここでビジターパターンをいつ/どのように使用するかに関するいくつかの投稿とそのいくつかの記事/章を読みました、そしてあなたがASTを横断していてそれが高度に構造化されていてそしてあなたがカプセル化したいならそれは理にかなっていますロジックを別の「ビジター」オブジェクトなどに変換します。しかし、Rubyでは、ブロックを使用してほぼ同じことを実行できるため、やり過ぎのように見えます。

Nokogiriを使用してxmlをpretty_printしたいと思います。著者は、ビジターパターンを使用することを推奨しました。これには、FormatVisitorなどを作成する必要があるため、「node.accept(FormatVisitor.new)」とだけ言うことができます。

問題は、FormatVisitorのすべてのもののカスタマイズを開始したい場合はどうなるかです(たとえば、ノードのタブ付け方法、属性の並べ替え方法、属性の間隔などを指定できます)。

  • あるとき、ノードにネストレベルごとに1つのタブを設定し、属性を任意の順序にする必要があります
  • 次回は、ノードに2つのスペースを設定し、属性をアルファベット順に設定します。
  • 次回は、スペースが3つ、1行に属性が2つあるものが必要です。

私にはいくつかの選択肢があります:

  • コンストラクターでオプションハッシュを作成します(FormatVisitor.new({:tabs => 2})
  • ビジターを作成した後に値を設定します
  • 新しい実装ごとにFormatVisitorをサブクラス化します
  • または、訪問者ではなく、ブロックを使用します

FormatVisitorを作成して値を設定し、それをnode.acceptメソッドに渡す代わりに、次のようにします。


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

これは、ビジターパターンが次のように見えると私が感じるものとは対照的です。


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

ビジターパターンが間違っているかもしれませんが、ルビーで読んだことからすると、やり過ぎのようです。どう思いますか?どちらの方法でも問題ありません。皆さんがそれについてどう感じているのか疑問に思っています。

どうもありがとう、ランス

4

2 に答える 2

17

本質的に、Rubyブロック、余分な定型文のないビジターパターンです。些細なケースでは、ブロックで十分です。

たとえば、Arrayオブジェクトに対して単純な操作を実行する場合は#each、個別のVisitorクラスを実装する代わりに、ブロックを使用してメソッドを呼び出すだけです。

ただし、特定の場合に具体的なビジターパターンを実装することには利点があります。

  • 複数の類似しているが複雑な操作の場合、Visitorパターンは継承を提供しますが、ブロックは提供しません。
  • ビジタークラス用に別のテストスイートを作成するためのクリーナー。
  • 複雑なスマートクラスを小さなダムクラスに分離するよりも、小さなダムクラスを大きなスマートクラスにマージする方が常に簡単です。

実装はやや複雑に見えます。Nokogiriは#visitメソッドを強制するVisitorインスタンスを想定しているため、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■ファクトリパターンを使用して、特定のノードに適切なフォーマッタをインスタンス化します。この場合、テキストノード、リーフ要素ノード、テキスト付き要素ノード、および通常の要素ノードを区別しました。タイプごとに異なるフォーマット要件があります。また、これは完全ではないことに注意してください。たとえば、コメントノードは考慮されません。

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他に何も変更せずに引数にプラグインできます。

使用されたデザインパターンのリスト:

  • ビジターパターン:ノードトラバーサルロジックを処理します。(また、ノコギリによるインターフェース要件。)
  • ファクトリパターン。ノードタイプやその他のフォーマット条件に基づいてフォーマッタを決定するために使用されます。のクラスメソッドが気に入らない場合は、NodeFormatterそれらを抽出してNodeFormatterFactoryより適切にすることができます。
  • 依存性注入(DI / IoC)、属性のフォーマットを制御するために使用されます。

これは、いくつかのパターンを組み合わせて、必要な柔軟性を実現する方法を示しています。ただし、これらの柔軟性が必要な場合は、決定する必要があります。

于 2012-03-22T18:56:57.550 に答える
8

シンプルで機能するものを選びます。詳細はわかりませんが、Visitorパターンと比べて書いた方がシンプルに見えます。それがあなたのためにも働くなら、私はそれを使うでしょう。個人的には、1つの小さな問題を解決するためだけに、相互に関連するクラスの巨大な「ネットワーク」を作成するように求めるこれらすべての手法にうんざりしています。

ええと言う人もいますが、パターンを使用してそれを行うと、多くの将来のニーズと何とか何とかをカバーすることができます。私は言います、今うまくいくことをしてください、そして必要が生じたら、あなたは将来リファクタリングすることができます。私のプロジェクトでは、その必要性はほとんど発生しませんが、それは別の話です。

于 2009-10-05T06:35:01.777 に答える