重构一个有太多(6 +)参数的方法的最佳方法是什么?

我偶尔会遇到一些方法,它们的参数数量多得令人不舒服。通常情况下,它们似乎是构造函数。看起来应该有更好的办法,但我不知道是什么办法。

return new Shniz(foo, bar, baz, quux, fred, wilma, barney, dino, donkey)

我曾经想过使用 structs 来表示参数列表,但是这似乎只是将问题从一个地方转移到另一个地方,并在过程中创建另一种类型。

ShnizArgs args = new ShnizArgs(foo, bar, baz, quux, fred, wilma, barney, dino, donkey)
return new Shniz(args);

所以这看起来不像是一个进步,那么最好的方法是什么呢?

84354 次浏览

最好的办法就是找到将论点归类在一起的方法。这是假设的,而且只有在您最终将得到多个参数“分组”的情况下才有效。

例如,如果要传递矩形的规范,可以传递 x、 y、 width 和 height,也可以只传递包含 x、 y、 width 和 height 的矩形对象。

在重构时寻找类似这样的东西,以便在某种程度上清理它。如果这些参数真的无法组合,那么开始考虑是否违反了单一责任原则。

您可以尝试将参数分组为多个有意义的 struct/class (如果可能的话)。

我认为你描述的方法是正确的。当我发现一个有很多参数的方法和/或一个将来可能需要更多参数的方法时,我通常会创建一个 ShnizParams 对象来传递,就像您描述的那样。

I would generally lean towards the structs approach - presumably the majority of these parameters are related in some way and represent the state of some element that is relevant to your method.

如果这组参数不能成为一个有意义的对象,这可能是 Shniz做得太多的迹象,重构应该包括将方法分解成单独的关注点。

如果它是一个构造函数,特别是如果有多个重载变量,那么您应该查看 Builder 模式:

Foo foo = new Foo()
.configBar(anything)
.configBaz(something, somethingElse)
// and so on

如果这是一个普通的方法,那么您应该考虑传递的值之间的关系,或许还可以创建一个传输对象。

这个问题的经典答案是使用一个类来封装部分或全部参数。理论上这听起来不错,但我是那种为领域中有意义的概念创建类的人,所以应用这个建议并不总是那么容易。

例如:

driver.connect(host, user, pass)

你可以用

config = new Configuration()
config.setHost(host)
config.setUser(user)
config.setPass(pass)
driver.connect(config)

YMMV

不要在构造函数中同时设置它,而是通过 属性/设定值进行设置,这样如何?我见过一些。NET 类使用这种方法,如 Process类:

        Process p = new Process();


p.StartInfo.UseShellExecute = false;
p.StartInfo.CreateNoWindow = true;
p.StartInfo.RedirectStandardOutput = true;
p.StartInfo.RedirectStandardError = true;
p.StartInfo.FileName = "cmd";
p.StartInfo.Arguments = "/c dir";
p.Start();

这取决于您具有什么样的参数,但是如果它们是大量的布尔值/选项,也许您可以使用 Flag Enum?

您可以用源代码行来交换复杂性。如果方法本身执行的任务太多(瑞士刀) ,则尝试通过创建另一个方法将其任务减半。如果方法很简单,只是需要太多的参数,那么所谓的参数对象就是方法。

我会使用缺省构造函数和属性设置器,C # 3.0有一些很好的语法可以自动完成。

return new Shniz { Foo = foo,
Bar = bar,
Baz = baz,
Quuz = quux,
Fred = fred,
Wilma = wilma,
Barney = barney,
Dino = dino,
Donkey = donkey
};

代码的改进在于简化了构造函数,而不必支持多个方法来支持各种组合。“调用”语法仍然有点“冗长”,但并不比手动调用属性设置器更糟糕。

I don't want to sound like a wise-crack, but you should also check to make sure the data you are passing around 真的 should be passed around: Passing stuff to a constructor (or method for that matter) smells a bit like to little emphasis on the 行为 of an object.

不要误解我的意思: 方法和构造函数 威尔有时有很多参数。但是当遇到这种情况时,请考虑用 行为来封装 data

这种味道(因为我们讨论的是重构,所以这个可怕的词似乎是合适的...)也可能被检测到对象有很多(read: any)属性或 getter/setter。

我认为这个问题与您试图用这个类解决的问题的领域紧密相关。

在某些情况下,一个包含7个参数的构造函数可能表明了一个糟糕的类层次结构: 在这种情况下,上面建议的 helper struct/class 通常是一个很好的方法,但是最终你也可能会得到大量的 struct,它们只是属性袋,并且没有做任何有用的事情。 8个参数的构造函数也可能表明您的类过于泛型/过于通用,因此它需要很多选项才能真正有用。在这种情况下,您既可以重构类,也可以实现静态构造函数来隐藏真正的复杂构造函数。Shniz.NewBaz (foo,bar)实际上可以调用传递正确参数的实际构造函数。

One consideration is which of the values would be read-only once the object is created?

可公开写入的属性也许可以在构造之后分配。

这些价值观最终从何而来?也许有些值确实是外部的,因为其他值确实来自库维护的某些配置或全局数据。

In this case you could conceal the constructor from external use and provide a Create function for it. The create function takes the truely external values and constructs the object, then uses accessors only avaiable to the library to complete the creation of the object.

如果一个对象需要7个或更多的参数来赋予对象完整的状态,并且所有这些都是真正的外部性质,那将是非常奇怪的。

If some of the constructor parameters are optional it makes sense to use a builder, which would get the required parameters in the constructor, and have methods for the optional ones, returning the builder, to be used like this:

return new Shniz.Builder(foo, bar).baz(baz).quux(quux).build();

这方面的细节在《有效的 Java 》第2版第11页有所描述。对于方法参数,同一本书(第189页)描述了三种缩短参数列表的方法:

  • 将该方法分解为多个使用较少参数的方法
  • 创建静态 helper 成员类来表示参数组,即传递一个 DinoDonkey而不是 dinodonkey
  • 如果参数是可选的,那么可以对方法采用上面的构建器,为所有参数定义一个对象,设置所需的参数,然后对它调用某个执行方法

当一个类有一个接受太多参数的构造函数时,这通常表明它有太多的责任。它可以被分解为不同的类,这些类相互协作以提供相同的功能。

如果您确实需要构造函数的那么多参数,那么 Builder 模式可以为您提供帮助。目标仍然是将所有参数传递给构造函数,因此它的状态从一开始就被初始化,如果需要,您仍然可以使类不可变。

见下文:

public class Toto {
private final String state0;
private final String state1;
private final String state2;
private final String state3;


public Toto(String arg0, String arg1, String arg2, String arg3) {
this.state0 = arg0;
this.state1 = arg1;
this.state2 = arg2;
this.state3 = arg3;
}


public static class TotoBuilder {
private String arg0;
private String arg1;
private String arg2;
private String arg3;


public TotoBuilder addArg0(String arg) {
this.arg0 = arg;
return this;
}
public TotoBuilder addArg1(String arg) {
this.arg1 = arg;
return this;
}
public TotoBuilder addArg2(String arg) {
this.arg2 = arg;
return this;
}
public TotoBuilder addArg3(String arg) {
this.arg3 = arg;
return this;
}


public Toto newInstance() {
// maybe add some validation ...
return new Toto(this.arg0, this.arg1, this.arg2, this.arg3);
}
}


public static void main(String[] args) {
Toto toto = new TotoBuilder()
.addArg0("0")
.addArg1("1")
.addArg2("2")
.addArg3("3")
.newInstance();
}


}

这引自福勒和贝克的书《重构》

长参数列表

在我们早期的编程时代,我们被教导把所有需要的东西作为参数传递进去 这是可以理解的,因为另一种选择是全局数据,而全局数据是 物体改变了这种情况,因为如果你没有 你总是可以要求另一个对象为你得到它。因此,对于对象,你不需要 传入方法需要的所有内容; 而是传入足够的内容,以便方法可以访问 方法需要的很多东西都可以在方法的宿主类中找到 面向对象程序的参数列表往往比传统的 程序。 这很好,因为长参数列表很难理解,因为它们会变成 不一致和难以使用,因为你永远改变他们,因为你需要 大多数更改都是通过传递对象来删除的,因为您更有可能 只需要发出几个请求就可以获得新的数据。 使用“将参数替换为方法”时,可以通过使用 一个你已经知道的对象的请求。这个对象可能是一个字段,也可能是 使用“保存整个对象”获取从 对象,并将其替换为对象本身 object, use Introduce Parameter Object. 进行这些更改有一个重要的例外 不希望创建从被调用对象到较大对象的依赖项 将数据解包并作为参数发送是合理的,但要注意痛苦 如果参数列表太长或变化太频繁,则需要重新考虑 从属关系结构。

如果您的语言支持它,那么使用命名参数并尽可能多地使用可选参数(使用合理的默认值)。

我同意将参数移动到参数对象(struct)中的方法。不过,与其把它们全部放在一个对象中,不如检查一下其他函数是否使用了类似的参数组。如果参数对象与多个函数一起使用,则更有价值,因为您希望参数集在这些函数之间一致地更改。可能只是将一些参数放入新的参数对象中。

如果有这么多参数,那么可能是该方法执行的操作太多了,所以首先要解决这个问题,将该方法拆分为几个较小的方法。如果在此之后仍然有太多的参数,请尝试对参数进行分组或将一些参数转换为实例成员。

更喜欢小的类/方法而不是大的。记住单一责任原则。

我假设你说的是 C # ,其中一些也适用于其他语言。

你有几个选择:

从构造函数切换到属性设置器 。这可以使代码更具可读性,因为对于读者来说,哪个值对应哪个参数是显而易见的。对象初始化器语法使这看起来不错。实现起来也很简单,因为您可以只使用自动生成的属性并跳过编写构造函数。

class C
{
public string S { get; set; }
public int I { get; set; }
}


new C { S = "hi", I = 3 };

但是,您失去了不可变性,并且失去了在编译时使用对象之前确保设置所需值的能力。

生成器模式

Think about the relationship between string and StringBuilder. You can get this for your own classes. I like to implement it as a nested class, so class C has related class C.Builder. I also like a fluent interface on the builder. Done right, you can get syntax like this:

C c = new C.Builder()
.SetX(4)    // SetX is the fluent equivalent to a property setter
.SetY("hello")
.ToC();     // ToC is the builder pattern analog to ToString()


// Modify without breaking immutability
c = c.ToBuilder().SetX(2).ToC();


// Still useful to have a traditional ctor:
c = new C(1, "...");


// And object initializer syntax is still available:
c = new C.Builder { X = 4, Y = "boing" }.ToC();

我有一个 PowerShell 脚本,它可以让我生成完成所有这些工作的构建器代码,其中的输入类似于:

class C {
field I X
field string Y
}

因此我可以在编译时生成。partial类允许我在不修改生成的代码的情况下扩展主类和构建器。

“引入参数对象”重构 。看看 重构目录。我们的想法是,将传递的一些参数放入一个新类型中,然后传递该类型的实例。如果你不假思索地这样做,你最终会回到起点:

new C(a, b, c, d);

变成了

new C(new D(a, b, c, d));

但是,这种方法最有可能对代码产生积极的影响。因此,请按照以下步骤继续:

  1. 一起查找有意义的参数的 子集。只是盲目地将函数的所有参数组合在一起并不能得到多少结果; 目标是要有合理的分组。当新类型的名称显而易见的时候,您就会知道您做对了。

  2. 寻找这些值一起使用的其他位置,并在那里也使用新类型。当您为一组已经在各处使用的值找到一个好的新类型时,这个新类型很可能在所有这些地方都有意义。

  3. 查找现有代码中但属于新类型的功能。

例如,您可能会看到一些类似于:

bool SpeedIsAcceptable(int minSpeed, int maxSpeed, int currentSpeed)
{
return currentSpeed >= minSpeed & currentSpeed < maxSpeed;
}

You could take the minSpeed and maxSpeed parameters and put them in a new type:

class SpeedRange
{
public int Min;
public int Max;
}


bool SpeedIsAcceptable(SpeedRange sr, int currentSpeed)
{
return currentSpeed >= sr.Min & currentSpeed < sr.Max;
}

这样更好,但要真正利用新类型的优势,将比较移动到新类型:

class SpeedRange
{
public int Min;
public int Max;


bool Contains(int speed)
{
return speed >= min & speed < Max;
}
}


bool SpeedIsAcceptable(SpeedRange sr, int currentSpeed)
{
return sr.Contains(currentSpeed);
}

而且 现在我们已经取得了一些进展: SpeedIsAcceptable()的实现现在说明了您的意思,并且您拥有了一个有用的、可重用的类。(下一个显而易见的步骤是将 SpeedRange变成 Range<Speed>。)

正如您所看到的,简介参数对象是一个很好的开始,但是它的真正价值在于它帮助我们发现了模型中缺少的一个有用的类型。

你没有提供足够的信息来保证一个好的答案。一个长的参数列表本质上并不坏。

Shniz(foo, bar, baz, quux, fred, wilma, barney, dino, donkey)

可解释为:

void Shniz(int foo, int bar, int baz, int quux, int fred,
int wilma, int barney, int dino, int donkey) { ...

在这种情况下,您最好创建一个类来封装参数,因为您以编译器可以检查的方式为不同的参数赋予了意义,并且可视化地使代码更容易阅读。它还使之后的阅读和重构更加容易。

// old way
Shniz(1,2,3,2,3,2,1,2);
Shniz(1,2,2,3,3,2,1,2);


//versus
ShnizParam p = new ShnizParam { Foo = 1, Bar = 2, Baz = 3 };
Shniz(p);

或者,如果你有:

void Shniz(Foo foo, Bar bar, Baz baz, Quux quux, Fred fred,
Wilma wilma, Barney barney, Dino dino, Donkey donkey) { ...

这是一个非常不同的情况,因为所有的对象都是不同的(而且不太可能被混淆)。同意如果所有对象都是必需的,并且它们都是不同的,那么创建一个参数类就没有什么意义了。

此外,一些参数是可选的吗?是否存在方法重写(相同的方法名,但方法签名不同?)这些细节对于 最好的的答案都很重要。

* 物业袋也可以有用,但由于没有提供背景资料,不一定更好。

正如你所看到的,这个问题的正确答案不止一个。

命名参数是消除长(甚至短)歧义的一个很好的选择(假设支持它们的语言)参数列表,同时允许(在构造函数的情况下)类的属性是不可变的,而不必强制要求它以部分构造的状态存在。

在进行这种重构时,我要寻找的另一种选择是将相关参数组作为一个独立对象来处理,这样做可能更好。使用前面答案中的 Recangle 类作为例子,这个构造函数接受 x、 y、 height 和 width 的参数,它可以将 x 和 y 分解成一个 Point 对象,允许您将三个参数传递给 Recangle 的构造函数。或者更进一步,将其设置为两个参数(UpperLeftPoint,LowerRightPoint) ,但这将是一个更彻底的重构。

当我看到长参数列表时,我的第一个问题是这个函数或对象是否做得太多了:

EverythingInTheWorld earth=new EverythingInTheWorld(firstCustomerId,
lastCustomerId,
orderNumber, productCode, lastFileUpdateDate,
employeeOfTheMonthWinnerForLastMarch,
yearMyHometownWasIncorporated, greatGrandmothersBloodType,
planetName, planetSize, percentWater, ... etc ...);

当然,这个例子是有意的荒谬,但是我见过很多真正的程序,它们的例子只是稍微不那么荒谬,其中一个类被用来存放许多几乎不相关或者不相关的东西,显然只是因为同一个调用程序需要两者,或者只是因为程序员碰巧同时想到了两者。有时候,简单的解决方案是将类分成多个部分,每个部分做自己的事情。

稍微复杂一点的是,当一个类确实需要处理多个逻辑事情时,比如客户订单和关于客户的一般信息。在这些情况下,为客户创建一个类,为订单创建一个类,并让它们在必要时相互通信。所以不是:

 Order order=new Order(customerName, customerAddress, customerCity,
customerState, customerZip,
orderNumber, orderType, orderDate, deliveryDate);

我们可以:

Customer customer=new Customer(customerName, customerAddress,
customerCity, customerState, customerZip);
Order order=new Order(customer, orderNumber, orderType, orderDate, deliveryDate);

当然,我更喜欢只有1个、2个或3个参数的函数,但有时我们不得不接受,实际上,这个函数需要很多参数,而且函数本身的数量并不会真正造成复杂性。例如:

Employee employee=new Employee(employeeId, firstName, lastName,
socialSecurityNumber,
address, city, state, zip);

是的,有很多字段,但是我们可能要做的就是把它们保存到数据库记录中,或者把它们放到屏幕上。这里没有太多的处理过程。

当我的参数列表变长时,我更希望能够为字段提供不同的数据类型。比如我看到一个函数,比如:

void updateCustomer(String type, String status,
int lastOrderNumber, int pastDue, int deliveryCode, int birthYear,
int addressCode,
boolean newCustomer, boolean taxExempt, boolean creditWatch,
boolean foo, boolean bar);

然后我看到它的名字是:

updateCustomer("A", "M", 42, 3, 1492, 1969, -7, true, false, false, true, false);

我很担心。看看这个电话,根本不清楚这些神秘的数字、代码和标志是什么意思。这只是自找麻烦。程序员可能很容易对参数的顺序感到困惑,并意外地切换两个参数,如果它们是相同的数据类型,编译器就会接受它。我更希望有一个签名,其中所有这些东西都是枚举,因此调用会传入 Type.Aactive 而不是“ A”和 CreditWatch.NO 而不是“ false”,等等。

简短的回答是:
您需要 将相关参数分组或 < strong > 重新设计我们的模型

下面的例子中,构造函数接受 8个参数

public Rectangle(
int point1X,
int point1Y,


int point2X,
int point2Y,


int point3X,
int point3Y,


int point4X,
int point4Y) {
this.point1X = point1X;
this.point1Y = point1Y;


this.point2X = point2X;
this.point2Y = point2Y;


this.point3X = point3X;
this.point3Y = point3Y;


this.point4X = point4X;
this.point4Y = point4Y;
}

把有关参数分组之后,
然后,构造函数将接受 < strong > ONLY 4个参数

public Rectangle(
Point point1,
Point point2,
Point point3,
Point point4) {
this.point1 = point1;
this.point2 = point2;
this.point3 = point3;
this.point4 = point4;
}


public Point(int x, int y) {
this.x = x;
this.y= y;
}

或者让构造函数更聪明,
在重新设计我们的模型之后
然后,构造函数将采用 < strong > ONLY 2参数

public Rectangle(
Point leftLowerPoint,
Point rightUpperPoint) {
this.leftLowerPoint = leftLowerPoint;
this.rightUpperPoint = rightUpperPoint;
}