【问题标题】:Solutions for "too many parameters" warning“参数过多”警告的解决方案
【发布时间】:2014-03-24 03:51:56
【问题描述】:

在我的一些方法中,有Too Many Parameters 并且很难维护和阅读 源代码。有时我会担心“他们是否以适当的顺序传递了适当的值?

我正在使用Checkstyle 作为我的 Eclipse 插件,这给了我超过 7 个参数的警告。

我不确定它可能只是一个编码标准,我不在乎。但是当通过 viewservicedao 传递许多参数时,我注意到它很难阅读,以后也很难修改次。

所以,我正在尝试使用...传递这些参数

  1. 许多对象Beans。但这给我带来了另一个问题,因为我的参数不会得到任何保证(不确定它们是否存在)

  2. HashMap 类型参数。但这可能会迫使我检查一些验证并尝试从方法调用方面匹配 keys

以上两种方法也可能会丢失编译时错误检查。 有什么减少参数数量的建议吗?

【问题讨论】:

  • 构造函数的具体情况另见stackoverflow.com/questions/222214/…
  • @Raedwald 感谢您提供非常有用的问题(或答案)链接。 接受答案的构建器设计模式将有助于解决我的问题。谢谢谢谢谢谢.......

标签: java optimization parameters parameter-passing


【解决方案1】:

传递 HashMap 在无类型脚本语言中是一种常见做法,但在 Java 中是一种不好的做法。它破坏了强类型的优势,而强类型是您在 Java 中获得生产力的一部分。换句话说,Java 编译器无法帮助您在开发过程中发现错误,而您更有可能在运行时发现它们。

如果您传递的参数在概念上是相关的,您可以像您提到的那样将它们分组到一个适当的对象中。例如,如果您要传递参数,例如 firstName、lastName、dateOfBirth 等,则可以改为传递具有这些属性的 Person 对象。这使用 OO 使您的设计更易于思考和维护。

如果我理解你的意思:“但这给我带来了另一个麻烦,因为我的参数不会得到任何保证(不确定是否包含)”,你可以强制执行保证当您的 Person 或 etc. 对象被实例化时,您需要。一种方法是使用不可变的 Person (等)对象:没有设置器,而是通过构造函数传递所有参数。如果它们不完全正确,则抛出 IllegalArgumentException。

祝你好运!

【讨论】:

  • ..object: no setters, instead pass all params via the constructor. 我最喜欢它。感谢您提供非常有用的事实。
  • 很高兴我的回复很有用。如果它是最适合您的答案,请务必接受。谢谢!
  • 嗨大灾变:关于赏金。对于像你这样的问题,真的没有详细的、规范的答案。你在寻求设计建议,而设计就是权衡取舍:你得到什么与你失去什么。当然,努力获得比放弃更多。因此,您的选择将取决于最容易阅读/维护的内容、程序的其余部分是什么样的、您的组织是否要求您遵守特定的编码风格,甚至您更喜欢哪种编码风格。但是,一种“规范”的想法是:不要使用 HashMap 来传递参数。祝你好运!
  • 你好。继续赏金主题,我个人认为赏金是有道理的。直到几年前,我还没有意识到这个非常常见的问题的最佳解决方案。不幸的是,流行的资源并没有提供令人满意的解决方案。它显示在给出的答案中 - 每个答案只解决问题的一部分。我很高兴能够分享我在这方面的发现(见我的回答)。希望更多的开发人员熟悉这个优雅且可维护的解决方案。
  • 嗨 Przemek:您的解决方案似乎很有趣——尽管发布 Scala 代码来回答 Java 问题对我来说似乎不合适。但是,赏金特别寻求“详细的规范答案”,当然,您的提议没有任何规范。事实上,你向后弯腰注意到它是多么不规范,因为,正如你在暗示同意我的情况下强调的那样,对于这样的问题,真的没有详细的、规范的答案。问题是关于设计的,正如你巧妙地证明的那样,设计是关于权衡,而不是经典。
【解决方案2】:

有一些技术可以减少参数数量;

  1. 使用最小化方法(将方法分解为多个方法,每个方法只需要参数的一个子集)
  2. 使用实用程序类(帮助类)来保存一组参数(通常是静态成员类)
  3. 将 Builder 模式从对象构造调整为方法调用。
  4. 尝试通过更好的架构设计来减少单独包之间的数据流。

参考一些标准的java书;

  • Java:如何编程
  • Java 头部优先
  • 有效的 Java

还可以尝试学习设计模式,这对于最佳编码实践非常有用。

  • 以人为本的设计模式

【讨论】:

  • 抱歉,我想我已经解释了您需要什么。如果你很注意,1.使用最小化的方法;您可以将验证与普通类分开维护。 2.使用实用程序类;当数据流发生更频繁时,您可以使用辅助实用程序类来保存所需的数据需求,也可以在其中使用辅助函数(参数转换,...​​(可能是验证))。如果您说出确切的问题以及您需要什么级别的详细信息,我们一定可以提供帮助。
  • 我指出了这些书籍,您可以在其中找到有关此的更多详细信息,因为即使在获得这些建议之后,您也需要确切地知道如何实施它们,而这些答案无法提供您需要的所有详细信息。例如:Effective Java,第 7 章,第 40 条:解释它。
  • 谢谢,非常感谢您的所有建议。
【解决方案3】:

在我提出我的建议之前,让我先对提出的建议提出一些警告。我冒昧地跳过了“臭”的解决方案 - HashMap 和 bean,因为您可以清楚地看到它们的缺点。

小心

  1. 盲目地使用辅助类来保存参数组。当团队具有凝聚力时,它们真的可以发光(就像 Mark Phillips 的回答一样),但否则它们会导致问题(本质上就像一个类型化的 HashMap)。我怀疑它们是否适用于将 7 个参数从视图传递到 DAO 层的问题。

  2. 最小化的方法在有意义的情况下也很棒(例如 Effective Java 书中的 List 示例)。我只是很少看到他们这样做的地方,所以我怀疑他们会解决你的问题。

  3. Builder 模式通常非常简洁,但是它只解决了一层的问题 - 它没有告诉您如何进一步向下传递参数。当你从视图中获得一个参数时,这是 DAO 中需要的,Builder 只会让你的代码膨胀,你仍然需要传递参数。

在我最终提出解决方案之前,让我挑战一个常见的隐含假设,即所有数据都需要以方法参数的形式通过堆栈传递。仅当您将处理对象设为应用程序或会话范围时,这才是正确的。当您在请求处理开始时创建所有相关对象时,约束就会消失。然后,您可以使用对象的构造函数只向它们传递必要的信息。

最佳方式:使用请求生命周期的对象

这在某种程度上类似于方法对象或命令模式。

为了应用此解决方案,您需要更改系统的入口点 - 通常这是某些视图层对象中的方法。让它负责两件事:

  • 请求创建对象图
  • 调用对象图根的执行/运行方法

第一步至关重要。在这里,您可以从每一层构造请求范围的对象:视图、服务和 DAO。对于这些对象中的每一个,您只需将所需的数据传递给它们的构造函数(例如,如果只在 DAO 中需要参数“userIP” - 例如用于审核 DB 访问,则仅将其传递给 DAO 请求对象)。请求对象还需要对其协作者的引用(例如需要 DAO 的服务) - 相应地通过构造函数传递这些。

第二步:当你设置好你的对象图后,只需在第一个对象上调用执行/运行方法(通常是视图层的对象)。

/** The example (in Scala) shows how your app's entry point could look like.
 *  The presented method belongs to an app-scoped view-layer object.      
 */
def delete(itemId: Id, userIP: IPAddress) {
  // Note, that only RepositoryHelperReq class is interested in the 
  // "itemId" and "userIP" parameters
  val repoReq = MainRepositoryReq(RepositoryHelperReq(itemId, userIP))
  val serviceReq = MainServiceReq(ServiceHelperReq(repoReq))
  val viewReq = MainViewReq(ViewHelperReq(serviceReq))

  viewReq.execute()
}

现在让我来回应一些对这种模式的预期批评。

批评反驳

  1. 有人会说,性能会受到影响,因为堆上会有更多的对象进行垃圾收集。 我会要求他们进行测量,因为通常不是对象分配会影响性能,而是对象保留(请参阅last presentation of Simon Ritter)。

  2. 有些人会询问应用程序或会话范围的数据,例如数据源或购物篮对象。这些对象仍然可以使用 - 您只需将它们注入到您的请求范围的对象中。

  3. 有些人会批评依赖结构,说视图应该只依赖于服务而不是DAO。这是有效的评论,只是注意,在经典的 web 应用程序中,你仍然有一个中心位置,这取决于所使用的每一层(通常称为“世界末日”的地方)。有时是 web.xml,有时是 Spring 应用程序上下文或 Guice 模块。如果您关心适当的依赖关系,我建议您将所有工厂逻辑放在这样的地方,让它实现一些视图层接口并注入到视图中。这样你的整个依赖结构将保持干净和模块化。

  4. 有人会说,流行的 DI 框架(主要是 Spring)对这种模式的支持很差。确实如此,您需要使用一个体面的 DI 库(如果您喜欢 Scala,则需要使用体面的 DI 库(Guice、Java 的 Dagger 或 Macwire),或者准备好与 Spring 抗衡才能做到这一点。

好处

  1. 没有长参数列表的味道
  2. 没有不连贯的“请求上下文”对象(MagicContainer antipattern
  3. "stamp"-coupling - 中间层不需要依赖传递的参数,因此它们可以独立发布,更易测试和更简洁
  4. 数据只在需要的地方使用 - 更容易测试,更少模拟
  5. 即使在其他方法失败的情况下也可以使用,例如当您没有要提取的内聚 ParameterObject 或当您无法轻松地将方法拆分为最小的正交方法时

学分

Miško Hevery 在他出色的博文How to do Everything Wrong with Servlets 中向我介绍了这个特定问题的解决方案,并在Managing Object Lifetimes 中明确指出(参见“更常见的违规”片段)。我要感谢他的帖子,因为在其他来源中很难找到关于这个特定问题的准确指导。

【讨论】:

  • 我很高兴我的回答很有帮助。祝你好运!
【解决方案4】:

第一种方法是要走的路,它本质上是封装(OO 的基本原则之一)。至于“但这给我带来了另一个麻烦,因为我的参数不会得到任何保证(不确定是否会包含)。” - 这是一个非常常见的问题,已使用 JSR 303 解决。这是一个非常基本的带有 JSR 303 验证注释的 bean 示例:

import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;

public class Book {

   @NotNull 
   private String title;

   @NotNull
   private String author;

   @Min(value=100)
   private int numOfPages;

   @NotNull
   private String isbn;

   ... ... ...
}

验证方法如下:

Book book = new Book();
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
Set<ConstraintViolation<Book>> violations = validator.validate(book);
for (ConstraintViolation<Book> violation : violations) {
   System.out.format("%s: %s%n",violation.getPropertyPath(), violation.getMessage());
}

这是运行验证的输出:

isbn: may not be null
numOfPages: must be greater than or equal to 100
author: may not be null
title: may not be null

延伸阅读:http://www.jroller.com/eyallupu/entry/jsr_303_beans_validation_using

但是,您不必进行此类手动验证,例如在 Spring MVC 中,您可以将 @javax.validation.Valid 放在传递给方法的 bean 上,它将自动得到验证。

【讨论】:

    【解决方案5】:

    一般来说,我会说,尽量减少方法/类的责任以减少参数的数量。但如果真的需要它们,不要让插件阻止你。

    【讨论】:

    • 我并不担心插件的警告信息,我试图描述它可能是编码标准,但我不确定。在大多数情况下,我想我曾经使用过 if nessary。所以,我假设我们添加了这些实际需要的参数。
    • 这个插件很可能就在大灾变所描述的场景中。当您将数据从视图传递到 DAO 时,不可能在调用链中的所有方法中都使用所有参数。大多数情况下,人们传递参数是因为他们没有意识到替代方案(很遗憾)。
    【解决方案6】:

    您必须能够通过以下选项摆脱这个问题。
    1. 建造者模式
    2. 伪命名参数方法将是一个很好的方法。(参考http://java.dzone.com/articles/named-parameters-java)(最适合不可变对象)
    3. 命令模式。

    仅当您使用方法中的所有 7 个参数时,上述选项才有效。 如果不是,那么这个 api 的设计方式就有缺陷。

    在考虑设计方法和 api 时,我发现以下链接很有帮助,
    http://www.infoq.com/presentations/effective-api-design

    【讨论】:

      【解决方案7】:

      构造函数具有长长的参数列表是很常见的,尤其是对于 DAO。很多时候,我发现遵守类似的属性命名约定对防止这种情况有很大帮助。

      自从我编写任何 java 代码以来已经有一段时间了,所以我需要传递一个 java 特定的示例。基本上,如果您有具有相同名称和可分配类型的属性,则可以实现一个克隆函数,该函数接受任何对象并将其映射到另一个对象。

      在 C# 中,我在这里讨论过这个问题:Best way to clone properties of disparate objects

      就类型安全而言,您可以控制自己的命名约定。如果 personId 总是意味着相同的东西,并且总是具有相同的类型,那么您将避免一些可能导致问题的常见问题。

      如果我没有提到使用反射并不总是一个好方法,那我就失职了。您需要考虑性能影响。另一个考虑因素是您在持久化之前验证结果对象。最后,小心浅拷贝。

      【讨论】:

        【解决方案8】:

        我不知道你的包是如何分离的,而当传输数据包含太多参数时,我的建议是创建一个新的对象,在 javaee.As 中调用 DTO(数据传输对象)你说,这不容易保证。否则,使用 CALLBACK 是一种更好的方法,可以轻松保证和扩展。没有什么是完美的,你会选择你关注的方面。
        回调被视为

        void function(Callback callback){
            callback.do();
        }
        

        这是我的建议,但不是完全正确的方法。也许对你有帮助。

        【讨论】:

        • DTO 是 Controller(Web) 或 Service 之间的一个新层。
        • 你能支持我一些链接吗?请 。我会要求你用一些例子来描述你描述的 DTOCallback。 :)
        • 我不认为回调的概念解决了 OP 的问题。 OP 中的问题是关于 API 混乱和由此产生的维护/可读性问题。回调更多地是关于在特定情况下构建逻辑控制流。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2012-01-02
        • 1970-01-01
        • 2022-10-26
        • 2023-02-21
        • 1970-01-01
        • 2020-07-17
        • 1970-01-01
        相关资源
        最近更新 更多