【问题标题】:Dry This Ruby Code干掉这段 Ruby 代码
【发布时间】:2016-02-06 04:27:24
【问题描述】:

我怎样才能干掉这段代码?

module TraverseTree
  def inorder_traverse root
    return nil unless root
    result = []
    result.concat inorder_traverse root.left if root.left
    result.push root.val
    result.concat inorder_traverse root.right if root.right
    result
  end

  def preorder_traverse root
    return nil unless root
    result = []
    result.push root.val
    result.concat preorder_traverse root.left if root.left
    result.concat preorder_traverse root.right if root.right
    result
  end

  def postorder_traverse root
    return nil unless root
    result = []
    result.concat postorder_traverse root.left if root.left
    result.concat postorder_traverse root.right if root.right
    result.push root.val
    result
  end
end

有没有一种基于函数名称以编程方式对代码排序的好方法?

谢谢!!

【问题讨论】:

    标签: ruby refactoring dry


    【解决方案1】:
    def traverse_recurse(root, options)
      return unless root
      options[:preorder].call(root.val) if options[:preorder]
      traverse_recurse(root.left, options)
      options[:inorder].call(root.val) if options[:inorder]
      traverse_recurse(root.right, options)
      options[:postorder].call(root.val) if options[:postorder]
    end
    
    def traverse_collect(root, type)
      result = []
      traverse_recurse(root, type => lambda { |val| result.push(val) })
      result
    end
    
    def preorder_traverse(root)
      traverse_collect(root, :preorder)
    end
    
    def inorder_traverse(root)
      traverse_collect(root, :inorder)
    end
    
    def postorder_traverse(root)
      traverse_collect(root, :postorder)
    end
    

    【讨论】:

    • 这太棒了!非常感谢!只是一个快速的后续问题,你认为这值得吗?我的意思是 unDRYed 和 DRYed 代码的长度几乎相同,而 DRYed 代码似乎更复杂。在现实世界的制作中,你会建议这样做吗?
    • @marwei 是的,毫无疑问。重复的代码是维护的噩梦。
    • 我认为@marwei 的担忧是完全正确的。他们的原始代码有一些重复,但其意图非常明确。 Chris 的代码本身还不错,但它不那么明显。 @marwei 的代码是相当自我记录的,但如果没有几行 cmets,我不会将 Chris 的 traverse_recurse 方法提交到我的 repo 中。可读性对于可维护性与 DRYness 一样重要。
    【解决方案2】:

    正如 Chris 的回答所指出的,这里肯定有消除重复的方法,但正如我在对他们的回答的评论中提到的,我认为您的原始代码非常好,因为它的 intent 非常清除。即使没有任何 cmets,我也可以立即知道每种方法的作用,我不想看到你失去它。

    但是,我确实看到了一种方法,您可以在不牺牲可读性的情况下摆脱一些样板。

    这是你的第一种方法:

    def inorder_traverse root
        return nil unless root
        result = []
        result.concat inorder_traverse root.left if root.left
        result.push root.val
        result.concat inorder_traverse root.right if root.right
        result
      end
    

    我首先想到的是result = []; ... (return) result。这通常是 Ruby 中的一种代码异味,但如何消除它并不是很明显,所以我会回来讨论它。

    跳出来的第二件事是这个方法在以root.left为参数调用inorder_traverse之前检查root.left是否为nil,这很好,但随后inorder_traverse立即检查其参数是否为nil。我们不需要这样做两次。

    如果我们消除这些后置条件检查,我们会得到这样的结果:

    def inorder_traverse(root)
      return unless root
      result = []
      result.concat(inorder_traverse(root.left))
      result.push(root.val)
      result.concat(inorder_traverse(root.right))
      result
    end
    

    这是不对的,但是,因为 Array#concat 会在 inorder_traverse 返回 nil 时引发 TypeError。我们可以通过使用带有 splat (*) 的 Array#push 来解决这个问题:当参数是一个数组时,它就像 concat 一样工作,而当参数是 nil 时,它就像带有一个空数组的 concat 一样工作:

    def inorder_traverse(root)
      return unless root
      result = []
      result.push(*inorder_traverse(root.left))
      result.push(root.val)
      result.push(*inorder_traverse(root.right))
      result
    end
    

    不过,您可能已经意识到,如果我们向 push 发送一个参数,我们可以将所有参数发送到一个 push,而不是调用 push 三次:

    def inorder_traverse(root)
      return unless root
      result = []
      result.push(
        *inorder_traverse(root.left),
        root.val,
        *inorder_traverse(root.right)
      )
      result
    end
    

    ...但请稍等。如果我们只是初始化一个空数组,将一堆元素压入其中,然后将其返回,那么为什么我们在初始化时不直接将这些元素放到数组上呢?

    所以:

    module TraverseTree
      def inorder_traverse(root)
        return unless root
        [ *inorder_traverse(root.left),
          root.val,
          *inorder_traverse(root.right) ]
      end
    
      def preorder_traverse(root)
        return unless root
        [ root.val,
          *preorder_traverse(root.left),
          *preorder_traverse(root.right) ]
      end
    
      def postorder_traverse(root)
        return unless root
        [ *postorder_traverse(root.left),
          *postorder_traverse(root.right),
          root.val ]
      end
    end
    

    附:您还可以做的另一件事是将return unless root 替换为root && ...(或root and ...)。我觉得这很诱人,但也有点臭,所以我把它留给你:

    def inorder_traverse(root)
      root && [
        *inorder_traverse(root.left),
        root.val,
        *inorder_traverse(root.right)
      ]
    end
    

    奖金

    我不可避免地开始思考如何才能真正消除上面的所有重复,并想出了下面的代码,它是仓促编写、未经测试且完全不明智的。但是写起来很有趣!

    module TraverseTree
      ORDERS = %i[preorder inorder postorder].each do |order|
        define_method(:"#{order}_traverse", 
          &method(:traverse_by).curry(order))
      end
    
      private
      def traverse_by(order, root)
        root && [
          traverse_by(order, root.left),
          traverse_by(order, root.right)
        ]
        .insert(ORDERS.index(order), root.val)
        .compact.flatten
      end
    end
    

    【讨论】:

    • 哇,非常感谢你带我完成了这个,@Jordan!我非常喜欢 splat 的想法。它实际上让我想知道 push 和 concat 之间的性能有何不同。你可能以前读过这篇文章,但我会在这里分享http://www.continuousthinking.com/2011/09/07/ruby_array_plus_vs_push.html
    • && 技巧真的很有趣,虽然我同意你的观点,因为它使代码的可读性降低了一些。这是另一个幼稚的问题:在生产环境中,您如何评价可读性、干燥度和性能的重要性?
    • 那是一篇很棒的文章。我很好奇 [].push(*foo)[ *foo ] 的比较;也许我下次在电脑前时会对其进行基准测试。
    • 写你的问题,我不知道他们是否可以排名,真的。 DRYness 对于可维护性很重要。如果您在应用程序的四个地方计算税率并且必须更改它,您可能会在一个地方错过它,因此最好将重复的代码移动到一种易于测试的方法中。可读性很重要,因为能够快速轻松地理解您正在阅读的代码可以减少您在进行更改时引入错误的可能性。 DRYness 和可读性可减少错误并节省开发成本。
    • 这是一个很好的答案。有几次我发现自己不干一些代码以获得更好的性能。感觉就像在可读性、干燥度和性能之间,我最多只能从三者中得到两个。你说的很有道理。谢谢@Jordan!
    【解决方案3】:

    如果你想在这件事上变得非常时髦,这里有一种实用的方法。

    left_vals = -> traverse_order, root { traverse_order[root.left] if root.left }
    right_vals = -> traverse_order, root { traverse_order[root.right] if root.right }
    current_val = -> traverse_order, root { root.val }
    traverse = -> parts, traverse_order, root { parts.inject([]) { |array, part| array.concat(Array(part[traverse_order, root])) } }
    inorder_traverse = traverse.curry.([left_vals, current_val, right_vals], -> root { inorder_traverse[root] })
    preorder_traverse = traverse.curry.([current_val, left_vals, right_vals], -> root { preorder_traverse[root] })
    postorder_traverse = traverse.curry.([left_vals, right_vals, current_val], -> root { postorder_traverse[root] })
    

    那你就可以打电话了;

    postorder_traverse[root]
    inorder_traverse.(root)
    preorder_traverse.call(root)
    

    它们都是等价的。

    【讨论】:

    • 转换它是一个有趣的挑战。实际上,除了密度之外,我发现它非常可读,我对不同解决方案的性能部分感到好奇,因为功能版本中没有状态。所以你为我提供了一个很好的测试用例。 :)
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-04-09
    • 2013-09-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多