72

高度重复的代码通常是一件坏事,有一些设计模式可以帮助减少这种情况。但是,有时由于语言本身的限制,这只是不可避免的。从以下示例中获取java.util.Arrays

/**
 * Assigns the specified long value to each element of the specified
 * range of the specified array of longs.  The range to be filled
 * extends from index <tt>fromIndex</tt>, inclusive, to index
 * <tt>toIndex</tt>, exclusive.  (If <tt>fromIndex==toIndex</tt>, the
 * range to be filled is empty.)
 *
 * @param a the array to be filled
 * @param fromIndex the index of the first element (inclusive) to be
 *        filled with the specified value
 * @param toIndex the index of the last element (exclusive) to be
 *        filled with the specified value
 * @param val the value to be stored in all elements of the array
 * @throws IllegalArgumentException if <tt>fromIndex &gt; toIndex</tt>
 * @throws ArrayIndexOutOfBoundsException if <tt>fromIndex &lt; 0</tt> or
 *         <tt>toIndex &gt; a.length</tt>
 */
public static void fill(long[] a, int fromIndex, int toIndex, long val) {
    rangeCheck(a.length, fromIndex, toIndex);
    for (int i=fromIndex; i<toIndex; i++)
        a[i] = val;
}

上面的代码片段在源代码中出现了 8 次,文档/方法签名几乎没有变化,但方法体完全相同,每个根数组类型int[], short[], char[], byte[], boolean[], double[],float[]Object[].

我相信,除非人们求助于反思(这本身就是一个完全不同的主题),否则这种重复是不可避免的。我知道作为一个实用程序类,如此高度集中的重复 Java 代码是非常不典型的,但即使采用最佳实践,重复也会发生!重构并不总是有效,因为它并不总是可能的(明显的情况是文档中有重复)。

显然维护这个源代码是一场噩梦。文档中的轻微错字或实现中的小错误会乘以多次重复。事实上,最好的例子恰好涉及到这个确切的类:

谷歌研究博客 - 额外的,额外的 - 阅读所有相关信息:几乎所有的二进制搜索和合并排序都被破坏了(作者 Joshua Bloch,软件工程师)

这个错误非常微妙,发生在许多人认为只是一种简单直接的算法中。

    // int mid =(low + high) / 2; // the bug
    int mid = (low + high) >>> 1; // the fix

上面的代码在源代码中出现了 11 次

所以我的问题是:

  • 在实践中如何处理这些重复的 Java 代码/文档?它们是如何开发、维护和测试的?
    • 你是从“原版”开始,尽可能的成熟,然后根据需要复制粘贴,希望你没有弄错?
    • 如果您确实在原始文件中犯了错误,那么只需在所有地方进行修复,除非您对删除副本并重复整个复制过程感到满意?
    • 你也对测试代码应用同样的过程吗?
  • Java 会从针对此类事物的某种有限使用源代码预处理中受益吗?
    • 也许 Sun 有自己的预处理器来帮助编写、维护、记录和测试这些重复的库代码?

评论要求另一个示例,所以我从 Google 集合中提取了这个示例:com.google.common.base.Predicates第 276-310 行(AndPredicate)与第 312-346 行(OrPredicate)。

这两个类的来源是相同的,除了:

  • AndPredicatevs OrPredicate(每个在同类中出现 5 次)
  • "And("vs Or("(在各自的toString()方法中)
  • #andvs #or(在@seeJavadoc 注释中)
  • truevs false(in apply;!可以改写出表达式)
  • -1 /* all bits on */0 /* all bits off */hashCode()
  • &=|=hashCode()
4

9 回答 9

32

对于绝对需要性能的人来说,装箱和拆箱以及通用系列和诸如此类的东西都是大忌。

同样的问题发生在性能计算中,您需要相同的复数来处理浮点数和双精度数(比如 Goldberd 的每个计算机科学家都应该了解浮点数论文中显示的一些方法)。

在处理类似数量的数据时,TroveTIntIntHashMap运行围绕 Java 运行是有原因的。HashMap<Integer,Integer>

现在 Trove collection 的源代码是如何编写的?

当然是通过使用源代码检测:)

有几个 Java 库可以提高性能(远高于默认的 Java 库),它们使用代码生成器来创建重复的源代码。

我们都知道“源代码检测”是邪恶的,代码生成是垃圾,但这仍然是真正知道自己在做什么的人(即编写像 Trove 这样的东西的人)这样做的方式:)

值得我们生成包含大警告的源代码,例如:

/*
 * This .java source file has been auto-generated from the template xxxxx
 * 
 * DO NOT MODIFY THIS FILE FOR IT SHALL GET OVERWRITTEN
 * 
 */
于 2010-02-26T04:06:51.940 回答
16

如果您绝对必须复制代码,请按照您给出的出色示例,将所有代码分组到一个地方,以便在您必须进行更改时轻松找到和修复。记录重复,更重要的是,记录重复的原因,以便每个追随你的人都知道这两者。

于 2010-02-25T20:10:52.430 回答
6

来自维基百科不要重复自己 (DRY) 或重复是邪恶的 (DIE)

在某些情况下,实施 DRY 理念所需的努力可能大于维护数据的单独副本所需的努力。在其他一些情况下,重复的信息是不可变的,或者受到足够严格的控制以使 DRY 不需要。

可能没有解决此类问题的方法或技术。

于 2010-02-25T20:20:25.673 回答
4

即使是像 Haskell 这样花哨的裤子语言也有重复的代码(请参阅我关于 Haskell 和序列化的帖子

这个问题似乎有三种选择:

  1. 使用反射并失去性能
  2. 对您的语言使用类似 Template Haskell 或 Caml4p 等预处理,并忍受肮脏的生活
  3. 或者我个人最喜欢的使用宏,如果你的语言支持的话(方案和 lisp)

我认为宏与预处理不同,因为宏通常与目标使用的语言相同,因为预处理是不同的语言。

我认为 Lisp/Scheme 宏可以解决很多这样的问题。

于 2010-04-28T01:02:00.990 回答
2

我知道 Sun 必须为 Java SE 库代码编写这样的文档,也许其他 3rd 方库编写者也这样做。

但是,我认为在仅在内部使用的代码中将文档复制和粘贴到这样的文件中是完全浪费的。我知道很多人会不同意,因为这会使他们内部的 JavaDocs 看起来不那么干净。然而,权衡是使他们的代码更干净,在我看来,这更重要。

于 2010-02-25T20:23:25.187 回答
2

Java 原始类型让您感到困惑,尤其是在涉及数组时。如果您特别询问涉及原始类型的代码,那么我会说尽量避免它们。如果您使用装箱类型,Object[] 方法就足够了。

一般来说,你需要大量的单元测试,除了诉诸反射之外,真的没有什么可做的。就像你说的,这完全是另一个话题,但不要太害怕反思。首先编写最干燥的代码,然后对其进行分析并确定反射性能是否真的差到足以保证编写和维护额外的代码。

于 2010-02-25T20:26:36.143 回答
2

您可以使用代码生成器来使用模板构造代码的变体。在这种情况下,java 源代码是生成器的产物,而真正的代码是模板。

于 2010-02-25T20:55:31.280 回答
2

鉴于两个声称相似的代码片段,大多数语言在构建将代码片段统一为一个整体的抽象方面的设施有限。当您的语言无法做到时,要抽象,您必须跳出语言:-{

最通用的“抽象”机制是一个完整的宏处理器,它可以在实例化“宏体”时将任意计算应用于“宏体”(想想Post 或具有图灵能力的字符串重写系统)。 M4GPM是典型的例子。C 预处理器不是其中之一。

如果您有这样的宏处理器,您可以将“抽象”构造为宏,并在您的“抽象”源文本上运行宏处理器以生成您编译和运行的实际源代码。

您还可以使用这些想法的更有限版本,通常称为“代码生成器”。这些通常没有图灵能力,但在许多情况下它们工作得很好。这取决于您的“宏实例化”需要有多复杂。(人们迷恋 C++ 模板机制的原因是尽管它很丑陋,但它具有图灵能力,因此人们可以用它完成真正丑陋但令人惊讶的代码生成任务)。这里的另一个答案提到了 Trove,它显然属于更有限但仍然非常有用的类别。

真正通用的宏处理器(如 M4)只处理文本;这使得它们功能强大,但它们不能很好地处理编程语言的结构,而且在这样一个不仅可以生成代码,还可以优化生成结果的 mcaro 处理器中编写生成器真的很尴尬。我遇到的大多数代码生成器都是“将此字符串插入此字符串模板”,因此无法对生成的结果进行任何优化。如果您希望生成任意代码并启动高性能,您需要具有图灵能力但了解生成代码的结构的东西,以便它可以轻松地操作(例如,优化)它)。

这样的工具称为程序转换系统。这样的工具就像编译器一样解析源文本,然后对其进行分析/转换以达到预期的效果。如果您可以在程序的源文本中放置标记(例如,具有它们的语言中的结构化注释或注释),以指导程序转换工具做什么,那么您可以使用它来执行此类抽象实例化、代码生成和/或代码优化。(一位发帖人建议挂钩到 Java 编译器是这个想法的一种变体)。使用通用的 puprose 转换系统(例如DMS Software Reengineering Tookit意味着您基本上可以对任何语言执行此操作。

于 2010-03-06T16:30:28.053 回答
1

由于泛型,现在可以避免很多这种重复。在编写仅类型更改的相同代码时,它们是天赐之物。

可悲的是,我认为泛型数组仍然没有得到很好的支持。至少现在,使用允许您利用泛型的容器。多态性也是减少这种代码重复的有用工具。

要回答您关于如何处理绝对必须重复的代码的问题...用易于搜索的注释标记每个实例。那里有一些 java 预处理器,它们添加了 C 风格的宏。我想我记得netbeans有一个。

于 2010-02-25T20:08:29.923 回答