偶尔我会遇到一些参数不舒服的方法.通常情况下,他们似乎是建设者.似乎应该有更好的方式,但我看不出它是什么.
return new Shniz(foo, bar, baz, quux, fred, wilma, barney, dino, donkey)
我曾想过使用结构来表示参数列表,但这似乎只是将问题从一个地方转移到另一个地方,并在流程中创建另一个类型.
ShnizArgs args = new ShnizArgs(foo, bar, baz, quux, fred, wilma, barney, dino, donkey) return new Shniz(args);
所以这似乎不是一种改进.那么最好的方法是什么?
我假设你的意思是C#.其中一些东西也适用于其他语言.
你有几个选择:
从构造函数切换到属性setter.这可以使代码更具可读性,因为对于读者来说,哪个值对应于哪些参数是显而易见的.Object Initializer语法使这看起来很漂亮.它实现起来也很简单,因为您可以使用自动生成的属性并跳过编写构造函数.
class C { public string S { get; set; } public int I { get; set; } } new C { S = "hi", I = 3 };
但是,您会失去不变性,并且在编译时使用对象之前,您将无法确保设置所需的值.
生成器模式.
想想string
和之间的关系StringBuilder
.你可以为自己的课程获得这个.我喜欢将它实现为嵌套类,因此类C
具有相关的类C.Builder
.我也喜欢构建器上的流畅界面.做得对,你可以得到这样的语法:
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));
但是,这种方法最有可能对您的代码产生积极影响.因此,请继续执行以下步骤:
寻找有意义的参数子集.只是盲目地将一个函数的所有参数组合在一起并不会让你感到太多; 目标是让分组有意义. 当新类型的名称显而易见时,你会知道你做对了.
寻找一起使用这些值的其他地方,并在那里使用新类型.很有可能,当你为一组已经在各处使用的值找到一个好的新类型时,新类型在所有这些地方也会有意义.
查找现有代码中的功能,但属于新类型.
例如,您可能会看到一些代码如下:
bool SpeedIsAcceptable(int minSpeed, int maxSpeed, int currentSpeed) { return currentSpeed >= minSpeed & currentSpeed < maxSpeed; }
您可以使用minSpeed
和maxSpeed
参数并将它们放在一个新类型中:
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
.)
正如您所看到的,引入参数对象是一个良好的开端,但它的真正价值在于它帮助我们发现了模型中缺少的有用类型.
最好的方法是找到将参数组合在一起的方法.这假定,并且只有当你最终会有多个"分组"参数时才会起作用.
例如,如果要传递矩形的规范,则可以传递x,y,width和height,或者只传递包含x,y,width和height的矩形对象.
在重构以稍微清理时,请查找类似的内容.如果参数确实无法合并,请开始查看您是否违反了单一责任原则.
如果它是构造函数,特别是如果存在多个重载变体,则应该查看Builder模式:
Foo foo = new Foo() .configBar(anything) .configBaz(something, somethingElse) // and so on
如果这是一种常规方法,您应该考虑传递的值之间的关系,并且可能创建一个传输对象.
这是引自福勒和贝克的书:"重构"
长参数列表
在我们的早期编程日,我们被教导作为参数传递例程所需的一切.这是可以理解的,因为替代方案是全球数据,全球数据是邪恶的,通常是痛苦的.对象改变了这种情况,因为如果你没有你需要的东西,你可以随时让另一个对象为你获取它.因此,对于对象,您不会传递方法所需的所有内容; 相反,你传递足够的方法,以便该方法可以得到它需要的一切.方法的主机类提供了许多方法所需的内容.在面向对象的程序中,参数列表往往比传统程序小得多.这很好,因为长参数列表很难理解,因为它们变得不一致且难以使用,因为你需要更多数据,所以你永远在改变它们.通过传递对象可以消除大多数更改,因为您更有可能只需要生成几个请求来获取新数据.如果可以通过发出对已知对象的请求来获取一个参数中的数据,请使用"将参数替换为方法".此对象可能是字段,也可能是其他参数.使用"保留整个对象"来获取从对象收集的大量数据,并将其替换为对象本身.如果您有多个没有逻辑对象的数据项,请使用Introduce Parameter Object.进行这些更改有一个重要的例外.这是当您明确不希望从被调用对象创建对较大对象的依赖关系时.在这些情况下,解压缩数据并将其作为参数发送是合理的,但要注意所涉及的痛苦.如果参数列表太长或更改太频繁,则需要重新考虑依赖关系结构.
对此的经典答案是使用类来封装部分或全部参数.理论上听起来很棒,但我是那种为在域中有意义的概念创建类的人,所以应用这个建议并不总是那么容易.
例如,而不是:
driver.connect(host, user, pass)
你可以用
config = new Configuration() config.setHost(host) config.setUser(user) config.setPass(pass) driver.connect(config)
因人而异
我不想听起来像一个明智的裂缝,但你也应该检查以确保你传递的数据确实应该传递:将东西传递给构造函数(或者相关的方法)闻起来有点像很少强调对象的行为.
不要误会我的意思:方法和构造将有很多的参数的时候.但遇到这种情况时,请尝试考虑使用行为封装数据.
这种气味(因为我们正在谈论重构,这个可怕的词似乎合适......)也可能被检测到具有很多(读取:任何)属性或getter/setter的对象.
当我看到长参数列表时,我的第一个问题是这个函数或对象是否做得太多了.考虑:
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.ACTIVE而不是"A"和CreditWatch.NO而不是"false"等.
如果某些构造函数参数是可选的,那么使用构造函数构建器中获取所需参数的构建器是有意义的,并且具有可选构件的方法,返回构建器,如下所示:
return new Shniz.Builder(foo, bar).baz(baz).quux(quux).build();
有关详细信息,请参阅Effective Java,2nd Ed.,p.11.对于方法参数,同一本书(p.189)描述了三种缩短参数列表的方法:
将方法分解为多个参数较少的方法
创建静态助手成员类来表示参数组,即传递a DinoDonkey
而不是dino
和donkey
如果参数是可选的,则上面的构建器可用于方法,为所有参数定义对象,设置所需参数,然后在其上调用一些execute方法