跟踪实用类

19

最近我越来越对在项目代码库中出现的问题感到沮丧。

我正在开发一个有超过100万行代码的大型Java项目。接口和类结构设计得非常好,编写代码的工程师也非常熟练。问题是,在试图使代码更清晰的情况下,人们每当需要重用某些功能时就会编写Utility类,导致随着项目的不断增长,越来越多的Utility方法会出现。然而,当下一个工程师遇到相同的功能需求时,他无法知道某人已经在代码中的某个地方实现了Utility类(或方法),并在不同的类中实现另一个副本的功能。结果就是代码冗余和过多的具有重叠功能的Utility类。

是否有任何工具或任何设计原则,我们作为团队可以实施以防止Utility类的重复和低可见性?

示例:工程师A有3个地方需要将XML转换为String,因此他编写了一个名为XMLUtil的Utility类,并将一个静态的toString(Document)方法放入其中。工程师B有几个地方需要将Document序列化为各种格式,包括String,因此他编写了一个名为SerializationUtil的Utility类,并有一个名为serialize(Document)的静态方法返回一个String。

请注意,这不仅仅是代码重复,因为上面示例中的2种实现可能不同(比如一种使用转换器API,另一种使用Xerces2-J),因此这也可以看作是一个“最佳实践”问题...

更新:我想我最好描述一下我们目前的开发环境。 我们使用Hudson进行CI,Clover进行代码覆盖率检查,Checkstyle进行静态代码分析。 我们采用敏捷开发,包括每日会议和(可能不足的)代码审查。 我们将所有的Utility类定义在.util中,由于其大小现在已经有13个子包,在根(.util)类下大约有60个类。我们还使用第三方库,比如大多数apache commons jar和一些组成Guava的jar。

我相信如果我们让某人负责重构整个软件包,我们可以将公用程序的数量减少一半。我想知道是否有任何工具可以使这项任务更加节省成本,以及是否有任何方法可以尽可能地延迟问题再次发生。


请参见https://dev59.com/r2855IYBdhLWcg3wsWv3,了解如何按类型签名搜索Java API方法。 - meriton
我想你也可以将该解决方案调整为列出所有具有重复类型签名的方法。 - meriton
9个回答

9
解决这个问题的好方法是开始添加更多的面向对象编程。以您的示例为例:
示例:工程师A有3个地方需要将XML转换为字符串,因此他编写了一个实用类XMLUtil,并在其中放置了一个静态toString(Document)方法。
解决方案是停止使用原始类型或JVM提供的类型(String、Integer、java.util.Date、java.w3c.Document),并将它们包装在您自己的项目特定类中。然后,您的XmlDocument类可以提供方便的toString方法和其他实用方法。您自己的ProjectFooDate可以包含解析和格式化方法,否则这些方法将分散在各种DateUtils类中等等。
这样,当您尝试处理对象时,IDE将提示您使用实用程序方法。

是的,肯定要朝着更多的领域驱动模型/六边形架构发展。 - Stephan Eggermont

6
你遇到的问题非常普遍,也是一个真正的问题,因为没有好的解决方案。我们在这里面处于同样的情况,我甚至可以说更糟糕,我们有1300万行代码、人员变动频繁,超过800名开发人员在编写代码。我们经常讨论你所描述的相同问题。
第一个想法——你的开发人员已经使用过的——是将共同的代码重构成一些实用类。我们对这个解决方案的问题在于,即使进行了配对编程、指导和讨论,对于我们来说,人员太多了,这种方法并不有效。事实上,我们分成子团队,人们在自己的子团队中分享知识,但知识并没有在团队之间传递。也许我们错了,但我认为即使是配对编程和讨论,在这种情况下也无济于事。
我们还有一个架构团队。这个团队负责处理设计和架构问题,并制作我们可能需要的公共实用程序。这个团队实际上生产了我们可以称之为公司框架的东西。是的,它是一个框架,有时候它效果很好。这个团队还负责推动最佳实践,并提高应该做什么或不做什么、可用或不可用的意识。
良好的Java核心API设计是Java成功的原因之一。好的第三方开源库也很重要。即使是一个小巧的精心制作的API,也可以提供一个非常有用的抽象,并帮助大大减少代码大小。但你知道,制作框架和公共API与仅编写2小时的实用程序类完全不同。它的成本非常高。一个实用程序类需要2小时进行初始编码,也许需要2天进行调试和单元测试。当你开始在大型项目/团队中共享通用代码时,你真正制作了一个API。你必须确保完美的文档,真正易读易维护的代码。当你发布这个代码的新版本时,你必须保持向后兼容。你必须在公司范围内(或至少在团队范围内)推广它。从你的小实用程序类的2天,你会增长到10天、20天甚至50天的全面API。
而且,你的API设计可能并不是很好。嗯,这并不是说你的工程师不聪明——事实上他们很聪明。但是你愿意让他们在一个只是帮助UI以一致的方式解析数字的小实用程序类上工作50天吗?你愿意让他们在你开始使用具有完全不同需求的移动UI时重新设计整个东西吗?此外,你是否注意到世界上最聪明的工程师制作的API永远不会流行或者会缓慢消失?你看,我们制作的第一个Web项目只使用内部框架或根本没有框架。然后我们添加了PHP/JSP/ASP。然后在Java中我们添加了Struts。现在JSF是标准。我们正在考虑使用Spring Web Flow、Vaadin或Lift...
我想说的是,没有好的解决方案,随着代码大小和团队规模的增长,开销呈指数级增长。共享大型代码库会限制你的敏捷性和响应能力。任何变化都必须小
但在软件公司中,主要的生产力点并不是当解析XML时获得10甚至50行代码。一个通用的代码将会增长到一千行代码,重新创建了一个复杂的API,将被实用类层叠。当这个人为解析XML创建一个实用类时,这是很好的抽象。他给十几行甚至一百行的专业代码取了一个名字。这段代码是有用的,因为它很专业化。通用的API允许在流、URL、字符串等上工作。它有一个工厂,所以您可以选择解析器的实现。实用程序类很好,因为它只与此解析器和字符串一起使用,并且只需要一行代码来调用它。但是,这种实用程序代码显然是有限的。它适用于此移动应用程序或加载XML配置。这就是为什么开发人员首先添加了它的实用程序类。

总之,在尝试整合整个代码库的代码之前,我认为应该根据团队的增长将代码责任分割:

  • 将你的大型团队转变为小型团队,以处理几个子项目;
  • 确保接口良好,最小化集成问题,但让团队拥有自己的代码;
  • 在这些团队和相应的代码库内,确保您拥有最佳实践。不要有重复代码,好的抽象。使用来自社区的现有经过验证的API。使用配对编程、强大的API文档、wikis...但是你应该真正让不同的团队做出自己的选择,构建自己的代码,即使这意味着在团队之间或不同设计决策之间存在重复代码。你知道,如果设计决策不同,这可能是因为需求不同。

你真正管理的是复杂性。最后,如果你创建一个单体的代码库,一个非常通用和先进的代码库,你将增加新人上手的时间,增加开发人员不使用你的共同代码的风险,并且会减慢所有人的速度,因为任何更改都有很大的机会破坏现有的功能。


谢谢你的回答,我认为你是正确的。我们下一步的逻辑是将我们的代码分解成子项目,并在共享代码最小的地方定义接口。我们已经讨论了一段时间,我觉得这将提供我们所寻求的妥协。由于我不确定是否存在任何可以帮助我解决这个问题的工具,所以我将奖励授予你,因为你可能最接近定义缓解问题的方法。 - Asaf
50天分配给800个潜在用户并不是太大的负担。 - Nicola Musatti
没有人愿意为一件本来只需要两天完成的事情付出50天的时间。如果你不得不这样做,因为你有一个庞大的团队,那么这个庞大的团队实际上正在降低生产力。 - Nicolas Bousquet

4

有几种敏捷/ XP 实践可以用来解决这个问题,例如:

  • 相互交流(例如在每日站立会议期间)
  • 成对编程/ 代码审查

然后创建、记录和测试一个或多个可引用的实用库项目。我建议使用Maven来管理依赖项/ 版本。


我们正在通过将更多的实用程序外部化到不同的JAR包中(有点像Apache Commons)来逐步向这个解决方案迈进。然而,仍然存在一个问题,即如何对这些JAR包中的实用程序进行排序(哪些属于哪里,我应该在哪里查找)。我知道沟通至关重要,但不同的人负责代码的不同部分,如果没有一种工程师可以访问数据的方式,他就会错过某些东西。 - Asaf
也许我错了,但一个有100万行代码的代码库太大了,所以你不能仅通过交流和配对编程来管理代码重复。 - Nicolas Bousquet

3
您可以考虑建议将所有实用类放置在一个组织良好的包结构中,例如com.yourcompany.util。如果人们愿意很好地命名子包和类,那么至少如果他们需要找到一个实用程序,他们就知道该去哪里找了。我认为在这里没有什么万能答案。沟通很重要。也许如果开发人员在编写新实用程序时向其余开发人员发送简单的电子邮件,那就足以引起人们的注意。或者共享wiki页面,人们可以在上面列出/记录它们。

我们确实使用.util包结构,但是随着工具数量的增加,找到适合工作的正确工具成为一个问题,这又导致了更多的类。关于所有可用工具的Wiki可能是一个选择,但是在Wiki中找到信息与在Javadoc中找到信息大致相同。 - Asaf

1
  1. 团队沟通(大喊“嘿,有人有Document toString吗?”)
  2. 尽量减少实用类的数量,并将其限制在单个命名空间中
  3. 始终思考:如何使用对象来完成此操作。在您的示例中,我会扩展Document类并向其中添加那些toStringserialize方法。

我相信我们在保持基类中的常见功能方面正在做出良好的努力,然而,往往情况下扩展不是一个选项,org.w3c.Document 就是一个典型的例子。也许我们可以更好地将更多逻辑移入基类,但是如何找到所有现有的实用方法并将其移入基类呢? - Asaf
嗯,其中一种方法是在相关的文件夹中使用grep查找 "public static" 或类似模式。还有,我也感受到你对于 "sealed" 基类的看法。在这种情况下,您可以始终使用装饰模式将文档包装成自己的版本。 - Milimetric

0

当将IDE的“代码补全”功能与支持类型扩展的语言(如C#和F#)结合时,可以解决此问题。因此,想象一下Java拥有这样的功能,程序员可以在IDE中轻松地探索类的所有扩展方法:

Document doc = ...
doc.to //list pops up with toXmlString, toJsonString, all the "to" series extension methods

当然,Java没有类型扩展。但是,您可以使用grep搜索项目中“所有以SomeClass作为第一个参数的静态公共方法”,以获得有关为给定类编写了哪些实用程序方法的类似洞察力。

你知道有什么工具或Eclipse插件可以轻松完成这个吗?在添加任何静态方法之前要求所有程序员对其util包进行grep似乎很麻烦,我担心这个请求最终会被忽略,因为它过于复杂。 - Asaf
1
抱歉@Asaf,我不知道有这样一个现成的工具。但我认为这可能是一个有趣的小项目。我不经常使用grep,但我想模式应该相当简单且易于重用于任何类。一旦你掌握了这个技巧,你可以创建一个脚本,以类名作为唯一输入并运行命令。我认为这将在所有程序员身上减轻负担,完成工作。但是,如果有一个Eclipse插件,在类名上右键单击并选择(“查找所有实用方法”)...那也可能是一个有趣的项目! - Stephen Swensen
为了感谢你给我下一次“免费”冲刺提供思路,我会点赞你的评论。 - Asaf

0

构建一个能够识别“相同功能”的工具非常困难。(从理论上讲,这实际上是不可能的,而在实践中,您可能需要一个定理证明器才能做到这一点)。

但通常发生的情况是,人们克隆接近他们想要的代码,并进行自定义。种类型的代码可以使用克隆检测器找到。

我们的CloneDR是一种基于参数化语法树来检测精确和近似克隆代码的工具。它匹配代码的解析版本,因此不会被布局、更改的注释、修订后的变量名称或在许多情况下插入或删除的语句所混淆。它适用于许多语言(C++、COBOL、C#、Java、JavaScript、PHP等),您可以在提供的链接中查看克隆检测运行的示例。它通常会发现10-20%的重复代码,如果您将该代码抽象成库方法并坚持使用,您的代码库实际上可以缩小(有一个组织使用CloneDR就发生了这种情况)。


0

如果您正在寻找一种可以帮助您管理这个不可避免的问题的解决方案,那么我可以建议一个工具:

  • TeamCity:一个令人惊叹且易于使用的产品,可以从您的代码库中管理所有自动化代码构建并运行单元测试等。
    对于大多数人来说,它甚至是免费的产品。
    更好的部分:它内置了跨所有代码的重复代码检测

更多阅读材料:


0
  1. 标准应用程序实用项目。构建一个具有受限可扩展性范围和基于功能的包的jar。
  2. 使用常见的实用工具,如apache-commons或google collections,并提供抽象。
  3. 维护知识库和文档以及JIRA跟踪错误和增强功能。
  4. 进化重构。
  5. 使用findbugs和pmd查找代码重复或错误。
  6. 审查和测试实用工具的性能。
  7. 利用karma!要求团队成员为现有的混乱代码或需要新代码的情况做出贡献。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接