重构具有太多(6+)参数的方法的最佳方法是什么?
偶尔我遇到一些不舒服的参数方法。 往往不是,他们似乎是build设者。 似乎应该有一个更好的办法,但我不明白是什么。
return new Shniz(foo, bar, baz, quux, fred, wilma, barney, dino, donkey)
我曾经想过使用结构体来表示参数列表,但是这只是将问题从一个地方转移到另一个地方,并在这个过程中创build了另一个types。
ShnizArgs args = new ShnizArgs(foo, bar, baz, quux, fred, wilma, barney, dino, donkey) return new Shniz(args);
所以这似乎不是一个改进。 那么最好的方法是什么?
最好的办法是find方法将论据分组在一起。 这假设,而且只有在最后会有多个“分组”的论点时才会起作用。
例如,如果您正在通过矩形的规范,则可以传递x,y,宽度和高度,也可以传递包含x,y,宽度和高度的矩形对象。
寻找这样的事情,当重构清理它有点。 如果这些论点真的不能合并,就要开始考虑是否违反了单一责任原则。
我将假设你的意思是C# 。 其中一些内容也适用于其他语言。
你有几个select:
从构造函数切换到属性设置器 。 这可以使代码更具可读性,因为读者明白哪个值对应于哪些参数。 对象初始化器的语法使得这看起来不错。 实现起来也很简单,因为你可以使用自动生成的属性并跳过编写构造函数。
class C { public string S { get; set; } public int I { get; set; } } new C { S = "hi", I = 3 };
但是,您失去了不变性,并且在编译时使用该对象之前,将无法确保设置所需的值。
build造者模式 。
想想string
和StringBuilder
之间的关系。 你可以得到这个你自己的class级。 我喜欢把它作为一个嵌套类来实现,所以C
类有相关的类C.Builder
。 我也喜欢构build器上stream畅的界面。 做得对,你可以得到像这样的语法:
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脚本,可以让我生成构build器代码来完成所有这些,input如下所示:
class C { field IX field string Y }
所以我可以在编译时生成。 partial
类让我扩展主类和生成器,而不修改生成的代码。
“引入参数对象”重构 。 请参阅重构目录 。 这个想法是,你采取了一些你传递的参数,并把它们放入一个新的types,然后传递该types的实例。 如果你没有想到就这样做,那么你最终会回到你开始的地方:
new C(a, b, c, d);
变
new C(new D(a, b, c, d));
但是,这种方法最有可能对您的代码产生积极的影响。 所以,继续按以下步骤操作:
-
寻找一起合理的参数的子集 。 只是毫不费力地将一个函数的所有参数组合在一起并不会给你带来太多的帮助。 目标是要有合理的分组。 当新的名字显而易见的时候,你会知道你是对的。
-
寻找这些值一起使用的其他地方,并在那里使用新的types。 有机会,当你已经find了一个很好的新的types,你已经在各地使用的新的types,这种新的types也将在所有这些地方有意义。
-
查找现有代码中的function,但属于新types。
例如,也许你看到一些代码如下所示:
bool SpeedIsAcceptable(int minSpeed, int maxSpeed, int currentSpeed) { return currentSpeed >= minSpeed & currentSpeed < maxSpeed; }
你可以把minSpeed
和maxSpeed
参数放在一个新的types中:
class SpeedRange { public int Min; public int Max; } bool SpeedIsAcceptable(SpeedRange sr, int currentSpeed) { return currentSpeed >= sr.Min & currentSpeed < sr.Max; }
这样比较好,但要真正利用新的types,请将比较转换为新的types:
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>
。)
正如你所看到的,引入参数对象是一个好的开始,但是它的真正价值在于它帮助我们发现了一个从我们的模型中缺失的有用的types。
如果它是一个构造函数,特别是如果有多个重载的变体,你应该看看Builder模式:
Foo foo = new Foo() .configBar(anything) .configBaz(something, somethingElse) // and so on
如果这是一个正常的方法,你应该考虑正在传递的值之间的关系,也许创build一个传输对象。
对此的经典答案是使用一个类来封装一些或全部的参数。 理论上听起来不错,但我是那种在领域中有意义的概念创造类的人,所以应用这个build议并不容易。
例如,而不是:
driver.connect(host, user, pass)
你可以使用
config = new Configuration() config.setHost(host) config.setUser(user) config.setPass(pass) driver.connect(config)
因人而异
这是从福勒和贝克的书中引用的:“重构”
长参数列表
在我们早期的编程时间里,我们被教导要传入一个例程所需的所有东西。 这是可以理解的,因为替代scheme是全球数据,全球数据是邪恶的,通常是痛苦的。 对象改变这种情况,因为如果你没有你需要的东西,你总是可以请求另一个对象来为你获取它。 因此,对于你不传递方法所需的所有东西, 相反,你通过足够的方法可以得到它需要的一切。 方法的主机类上有很多方法需要。 在面向对象的程序中,参数列表往往比传统程序小得多。 这是很好的,因为长参数列表很难理解,因为它们变得不一致和难以使用,并且因为您需要更多的数据而永久地改变它们。 大部分更改都是通过传递对象来消除的,因为更有可能只需要几个请求来获得新的数据。 当你可以通过提出一个你已经知道的对象的请求来获取一个参数中的数据的时候,用方法replace参数。 这个对象可能是一个字段,也可能是另一个参数。 使用保留整个对象从对象中搜集一堆数据,并用对象本身replace它。 如果有多个数据项没有逻辑对象,请使用引入参数对象。 进行这些更改有一个重要的例外。 这是当你明确不想创build从被调用的对象到更大的对象的依赖。 在这些情况下,解包数据并将其作为参数发送是合理的,但要注意所涉及的痛苦。 如果参数列表太长或者经常更改,则需要重新考虑依赖关系结构。
我不想听起来像一个明智的破解,但你也应该检查,确保你传递的数据真的应该传递:传递给构造函数(或方法)的味道有点像很less强调对象的行为 。
不要误解我的意思:方法和构造函数有时会有很多参数。 但遇到时,请尽量考虑用行为封装数据 。
这种气味(因为我们正在谈论重构,这个可怕的词似乎是适当的…)也可能检测到有很多(读:任何)属性或getters / setters的对象。
如果某些构造函数的参数是可选的,那么使用一个构build器将会在构造器中获得所需的参数,并为可选的构造器提供方法,返回构build器,这样使用:
return new Shniz.Builder(foo, bar).baz(baz).quux(quux).build();
有关这方面的细节在Effective Java,2nd Ed。,p。 11.对于方法参数,同一本书(第189页)描述了缩短参数列表的三种方法:
- 将该方法分解为多个方法,这些方法只需要less量参数
- 创build静态助手成员类来表示参数组,即传递一个
DinoDonkey
而不是dino
和donkey
- 如果参数是可选的,则可以采用上面的构build器来为所有参数定义一个对象,设置所需的参数,然后调用一些执行方法
当我看到长参数列表,我的第一个问题是这个函数或对象是否做得太多。 考虑:
EverythingInTheWorld earth=new EverythingInTheWorld(firstCustomerId, lastCustomerId, orderNumber, productCode, lastFileUpdateDate, employeeOfTheMonthWinnerForLastMarch, yearMyHometownWasIncorporated, greatGrandmothersBloodType, planetName, planetSize, percentWater, ... etc ...);
当然这个例子是故意荒谬的,但是我看过很多真实的程序,例子中只有一些稍微荒谬的例子,其中一个类被用来保存许多勉强相关或不相关的东西,显然只是因为同一个调用程序需要两个或者因为程序员碰巧同时想到了两者。 有时候,简单的解决办法是把class级分成多个部分,每个部分都有自己的function。
稍微复杂一点的是,一个类真的需要处理多个逻辑事物,例如客户订单和关于客户的一般信息。 在这种情况下,为顾客打造一个class级,为class级打造一个class级,让他们根据需要互相交stream。 所以,而不是:
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);
是的,这是一堆字段,但是可能我们要做的就是把它们保存到数据库logging中,或者把它们扔到屏幕上或者其他的东西上。 这里没有太多的处理。
当我的参数列表变长,我更喜欢如果我可以给字段不同的数据types。 就像我看到一个如下的函数一样:
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);
我感到担心。 看看这个电话,这些神秘的数字,代码和标志是什么意思,都不是很清楚。 这只是要求错误。 程序员可能很容易对参数的顺序感到困惑,并且意外地切换了两个参数,如果它们是相同的数据types,编译器就会接受它。 我宁愿有一个签名,所有这些东西都是枚举,所以一个调用通过Type.ACTIVE而不是“A”和CreditWatch.NO而不是“false”等。
我会使用默认的构造函数和属性设置。 C#3.0有一些很好的语法来自动执行此操作。
return new Shniz { Foo = foo, Bar = bar, Baz = baz, Quuz = quux, Fred = fred, Wilma = wilma, Barney = barney, Dino = dino, Donkey = donkey };
代码改进是简化构造函数,而不必支持多种方法来支持各种组合。 “调用”语法仍然有点“罗嗦”,但并不比手动调用属性设置更糟糕。
你可以尝试将你的参数分成多个有意义的struct / class(如果可能的话)。
你没有提供足够的信息来保证一个好的答案。 一个长参数列表本质上并不坏。
Shniz(foo,bar,baz,quux,fred,wilma,barney,dino,驴)
可以解释为:
void Shniz(int foo, int bar, int baz, int quux, int fred, int wilma, int barney, int dino, int donkey) { ...
在这种情况下,您最好创build一个类来封装参数,因为您可以通过编译器可以检查的方式赋予不同参数的含义,并且可视化地使代码更易于阅读。 这也使得以后更容易阅读和重构。
// 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) { ...
这是一个非常不同的情况,因为所有的对象都是不同的(而且不太可能被混淆)。 同意,如果所有的对象都是必需的,而且它们都是不同的,那么创build一个参数类是没有意义的。
另外,有些参数是可选的吗? 是否有方法覆盖(方法名称相同,但方法签名不同)?这些细节都是最重要的问题。
*一个物业袋也可以是有用的,但不是特别好,因为没有背景给出。
正如你所看到的,这个问题有超过1个正确的答案。 拿你的select。
我通常会倾向于结构化的方法 – 大概这些参数中的大部分都是以某种方式相关联的,并代表与您的方法相关的某些元素的状态。
如果这组参数不能成为一个有意义的对象,这可能是Shniz
做得太多的一个标志,而重构应该涉及将这个方法分解成单独的关注点。
如果您的语言支持它,请使用命名参数并尽可能多地使用可选的(有合理的默认值)。
我想你描述的方法是要走的路。 当我find一个有很多参数的方法和/或将来可能需要更多的参数时,我通常会创build一个ShnizParams对象来通过,就像你所描述的那样。
如何不一次在构造函数中设置它,而是通过属性/设置器来完成呢? 我见过一些使用这种方法的.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();
您可以交易源代码行的复杂性。 如果方法本身太多(瑞士刀)尝试通过创build另一种方法来减半任务。 如果方法简单,只需要太多的参数,那么所谓的参数对象就是要走的路。
我同意将参数移动到参数对象(结构)的方法。 不是把它们都粘在一个对象中,而是检查其他函数是否使用类似的参数组。 如果一个参数对象与多个函数一起使用,并且期望参数集在这些函数中一致地改变,那么这个参数对象就更有价值。 这可能只是你把一些参数放到新的参数对象中。
如果你有这么多的参数,那么很有可能这个方法做得太多了,所以首先把这个方法分解成几个更小的方法来解决这个问题。 如果在尝试对参数进行分组或将一些参数转换为实例成员之后仍然有太多的参数。
比较喜欢小类/方法。 记住单一责任原则。
命名参数是一个很好的select(假设支持它们的语言),用于消除长参数列表(或者甚至是短参数列表),同时也允许(在构造函数的情况下)类的属性是不可改变的,而不需要允许它存在在部分build造的状态。
我在寻找这种重构的其他选项将是一组相关的参数,可以更好地处理为一个独立的对象。 以先前答案中的Rectangle类为例,使用参数x,y,height和width的构造函数可以将x和y分解为Point对象,从而允许将三个parameter passing给Rectangle的构造函数。 或者稍微进一步,使它成为两个参数(UpperLeftPoint,LowerRightPoint),但这将是一个更激进的重构。
这取决于你有什么样的参数,但如果他们是很多布尔值/选项也许你可以使用一个标志枚举?
我认为这个问题与你正在努力解决的问题的领域密切相关。
在某些情况下,一个7参数构造函数可能会指示一个错误的类层次结构:在这种情况下,上面提到的helper struct / class通常是一个很好的方法,但是最终也会导致大量的结构体并没有做任何有用的事情。 8个参数的构造函数也可能表明你的类太泛化/太多,所以需要很多选项才能真正有用。 在这种情况下,您可以重构类或实现隐藏真正的复杂构造函数的静态构造函数:例如。 Shniz.NewBaz(foo,bar)实际上可以调用传递正确参数的实际构造函数。
一个考虑因素是哪个值在创build对象后将是只读的?
可以公开写入的属性也许可以在施工后分配。
最终的价值从何而来? 也许有些值真的是外部的,而其他值则来自图书馆维护的某些configuration或全局数据。
在这种情况下,您可以隐藏构造函数从外部使用,并提供一个创build函数。 create函数接受真正的外部值并构造对象,然后使用访问器仅可用于库来完成对象的创build。
如果有一个对象需要7个或更多的参数来给对象一个完整的状态,并且所有的对象都是外部的,那将是非常奇怪的。
当一个clas有一个构造函数需要太多的参数时,通常这是一个标志,它有太多的责任。 它可能可以分解成单独的类,合作提供相同的function。
如果你确实需要构造函数的参数,Builder模式可以帮助你。 目标是仍然将所有的parameter passing给构造函数,所以它的状态从一开始就被初始化,如果需要的话,你仍然可以使类不可变。
见下文 :
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(); } }