107

有时,我们必须编写接收许多参数的方法,例如:

public void doSomething(Object objA , Object objectB ,Date date1 ,Date date2 ,String str1 ,String str2 )
{
}

当我遇到这种问题时,我经常将参数封装到一个映射中。

Map<Object,Object> params = new HashMap<Object,Object>();
params.put("objA",ObjA) ;

......

public void doSomething(Map<Object,Object> params)
{
 // extracting params 
 Object objA = (Object)params.get("objA");
 ......
 }

这不是一个好的做法,将 params 封装到 map 中完全是在浪费效率。好消息是,干净的签名,易于添加其他参数,修改最少。这种问题的最佳做法是什么?

4

17 回答 17

160

Effective Java的第 7 章(方法)第 40 项(仔细设计方法签名)中,Bloch 写道:

有三种技术可以缩短过长的参数列表:

  • 将方法分解为多个方法,每个方法只需要参数的一个子集
  • 创建辅助类来保存一组参数(通常是静态成员类)
  • Builder 模式从对象构造调整为方法调用。

更多细节,我鼓励你买这本书,真的很值得。

于 2010-03-12T12:08:02.607 回答
76

使用带有神奇字符串键的地图是个坏主意。您会丢失任何编译时间检查,而且还不清楚所需的参数是什么。您需要编写非常完整的文档来弥补它。几周后你会在不看代码的情况下记住那些字符串是什么吗?如果你打错了怎么办?使用错误的类型?在运行代码之前,您不会发现。

而是使用模型。创建一个类,它将成为所有这些参数的容器。这样你就可以保持 Java 的类型安全。您还可以将该对象传递给其他方法,将其放入集合等。

当然,如果这组参数没有在其他地方使用或传递,那么专用模型可能会过大。有一个平衡要达到,所以使用常识。

于 2010-03-12T12:01:41.970 回答
26

如果您有许多可选参数,您可以创建流畅的 API:用方法链替换单个方法

exportWithParams().datesBetween(date1,date2)
                  .format("xml")
                  .columns("id","name","phone")
                  .table("angry_robots")
                  .invoke();

使用静态导入,您可以创建内部流畅的 API:

... .datesBetween(from(date1).to(date2)) ...
于 2010-03-13T19:51:35.737 回答
13

它被称为“引入参数对象”。如果您发现自己在多个地方传递相同的参数列表,只需创建一个包含所有参数的类。

XXXParameter param = new XXXParameter(objA, objB, date1, date2, str1, str2);
// ...
doSomething(param);

即使您没有发现自己经常传递相同的参数列表,这种简单的重构仍然会提高您的代码可读性,这总是好的。如果您在 3 个月后查看您的代码,当您需要修复错误或添加功能时会更容易理解。

这当然是一个普遍的理念,由于你没有提供任何细节,我也不能给你更详细的建议。:-)

于 2010-03-12T12:07:57.767 回答
10

首先,我会尝试重构该方法。如果它使用这么多参数,它可能会太长。分解它既可以改进代码,也可以减少每个方法的参数数量。您还可以将整个操作重构为它自己的类。其次,我会寻找使用相同参数列表的相同(或超集)的其他实例。如果您有多个实例,则很可能表明这些属性属于一起。在这种情况下,创建一个类来保存参数并使用它。最后,我将评估参数的数量是否值得创建地图对象以提高代码可读性。我认为这是一个个人电话——这个解决方案的每一种方式都有痛苦,而且权衡点可能会有所不同。对于六个参数,我可能不会这样做。10 我可能会(如果没有其他方法首先起作用)。

于 2010-03-12T12:07:10.283 回答
8

在构造对象时,这通常是一个问题。

在这种情况下,使用builder object pattern,如果您有大量参数并且并不总是需要所有参数,它会很好地工作。

您还可以将其调整为方法调用。

它还大大提高了可读性。

public class BigObject
{
  // public getters
  // private setters

  public static class Buider
  {
     private A f1;
     private B f2;
     private C f3;
     private D f4;
     private E f5;

     public Buider setField1(A f1) { this.f1 = f1; return this; }
     public Buider setField2(B f2) { this.f2 = f2; return this; }
     public Buider setField3(C f3) { this.f3 = f3; return this; }
     public Buider setField4(D f4) { this.f4 = f4; return this; }
     public Buider setField5(E f5) { this.f5 = f5; return this; }

    public BigObject build()
    {
      BigObject result = new BigObject();
      result.setField1(f1);
      result.setField2(f2);
      result.setField3(f3);
      result.setField4(f4);
      result.setField5(f5);
      return result;
    }
  }
}

// Usage:
BigObject boo = new BigObject.Builder()
  .setField1(/* whatever */)
  .setField2(/* whatever */)
  .setField3(/* whatever */)
  .setField4(/* whatever */)
  .setField5(/* whatever */)
  .build();

您还可以将验证逻辑放入 Builder set..() 和 build() 方法中。

于 2014-09-16T20:14:30.393 回答
7

有一种称为Parameter object的模式。

想法是使用一个对象代替所有参数。现在即使你以后需要添加参数,你只需要将它添加到对象中。方法接口保持不变。

于 2010-03-12T12:05:34.310 回答
5

您可以创建一个类来保存该数据。虽然需要足够有意义,但比使用地图(OMG)要好得多。

于 2010-03-12T11:53:21.187 回答
4

Code Complete* 提出了几点建议:

  • “将例程参数的数量限制在七左右。七是人们理解的神奇数字”(第 108 页)。
  • “将参数按输入-修改-输出的顺序排列……如果多个例程使用相似的参数,请将相似的参数按一致的顺序排列”(第 105 页)。
  • 将状态或错误变量放在最后。
  • 正如tvanfosson 所提到的,只传递例程需要的结构化变量(对象)的部分。也就是说,如果您在函数中使用大部分结构化变量,那么只需传递整个结构,但请注意这会在一定程度上促进耦合。

* 第一版,我知道我应该更新。此外,自 OOP 开始变得更流行时编写第二版以来,其中一些建议可能已经改变。

于 2010-03-15T08:52:08.490 回答
3

使用 Map 是清理调用签名的一种简单方法,但是您遇到了另一个问题。您需要查看方法的主体内部,以查看该方法在该 Map 中的预期内容、键名或值的类型。

一种更简洁的方法是将对象 bean 中的所有参数分组,但这仍然不能完全解决问题。

你这里有一个设计问题。如果一个方法的参数超过 7 个,您将开始难以记住它们代表什么以及它们的顺序。从这里你会得到很多错误,只是以错误的参数顺序调用方法。

您需要更好的应用程序设计,而不是发送大量参数的最佳实践。

于 2010-03-12T12:02:40.383 回答
2

好的做法是重构。这些对象意味着它们应该被传递给这个方法吗?是否应该将它们封装到单个对象中?

于 2010-03-12T11:53:19.970 回答
1

创建一个 bean 类,并设置所有参数(setter 方法)并将这个 bean 对象传递给该方法。

于 2010-03-12T12:24:19.693 回答
1
  • 查看您的代码,看看为什么所有这些参数都被传入。有时可以重构方法本身。

  • 使用地图会使您的方法容易受到攻击。如果有人使用您的方法拼错了参数名称,或者在您的方法需要 UDT 的地方发布了字符串怎么办?

  • 定义一个传输对象。它至少会为您提供类型检查;您甚至可以在使用点而不是在您的方法中执行一些验证。

于 2010-03-15T08:17:42.667 回答
1

我会说坚持你以前的做法。您的示例中的参数数量并不多,但替代方案要可怕得多。

  1. 地图 - 你提到的效率问题,但这里更大的问题是:

    • 来电者不知道要给您发送什么而不参考
      其他内容...您是否有 javadocs 可以准确说明使用了哪些键和
      值?如果你这样做(这很好),那么拥有很多参数也不是问题。
    • 接受不同的参数类型变得非常困难。您可以将输入参数限制为单一类型,也可以使用 Map<String, Object> 并强制转换所有值。大多数时候这两种选择都很糟糕。
  2. 包装器对象-这只是解决了问题,因为您首先需要填充包装器对象-而不是直接填充到您的方法,而是填充到参数对象的构造函数。确定移动问题是否合适取决于所述对象的重用。例如:

不会使用它:它只会在第一次调用时使用一次,所以很多额外的代码来处理 1 行......?

{
    AnObject h = obj.callMyMethod(a, b, c, d, e, f, g);
    SomeObject i = obj2.callAnotherMethod(a, b, c, h);
    FinalResult j = obj3.callAFinalMethod(c, e, f, h, i);
}

可以使用它:在这里,它可以做得更多。首先,它可以分解 3 个方法调用的参数。它本身也可以执行另外两条线......所以它在某种意义上成为一个状态变量......

{
    AnObject h = obj.callMyMethod(a, b, c, d, e, f, g);
    e = h.resultOfSomeTransformation();
    SomeObject i = obj2.callAnotherMethod(a, b, c, d, e, f, g);
    f = i.somethingElse();
    FinalResult j = obj3.callAFinalMethod(a, b, c, d, e, f, g, h, i);
}
  1. Builder 模式——在我看来,这是一种反模式。最理想的错误处理机制是更早检测,而不是更晚检测;但是使用构建器模式,缺少(程序员不认为包含它)强制参数的调用从编译时移动到运行时。当然,如果程序员故意将 null 或诸如此类的东西放在插槽中,那将是运行时,但仍然更早地捕获一些错误对于迎合拒绝查看他们正在调用的方法的参数名称的程序员来说是一个更大的优势。我发现它只在处理大量可选参数时才合适,即使这样,好处也只是微不足道的。我非常反对建造者“模式”。

人们忘记考虑的另一件事是 IDE 在这一切中的作用。当方法具有参数时,IDE 会为您生成大部分代码,并且您会用红线提醒您需要提供/设置什么。使用选项 3 时……你完全失去了这个。现在由程序员来做对了,在编码和编译期间没有任何线索……程序员必须对其进行测试才能找到答案。

此外,如果不必要地广泛采用选项 2 和 3,由于它会生成大量重复代码,因此在维护方面会产生长期的负面影响。代码越多,维护的越多,维护它所花费的时间和金钱就越多。

于 2020-08-02T17:53:12.890 回答
0

这通常表明您的班级承担了多个职责(即,您的班级做得太多)。

参见单一职责原则

了解更多详情。

于 2010-03-12T12:31:25.137 回答
0

如果您传递的参数过多,请尝试重构该方法。也许它正在做很多它不应该做的事情。如果不是这种情况,请尝试用单个类替换参数。这样,您可以将所有内容封装在单个类实例中并传递实例而不是参数。

于 2010-03-12T14:55:39.570 回答
0

... Bob 是你的叔叔:用于创建对象的轻松花哨的 API!

https://projectlombok.org/features/Builder

于 2021-10-04T13:33:17.060 回答