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


102

有时候,我会遇到参数数量不合理的方法。通常,它们似乎是构造函数。似乎应该有一个更好的方法,但是我看不到它是什么。

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);

因此,这似乎不是一种改善。那么最好的方法是什么?


你说“结构”。该术语在不同的编程语言中具有不同的含义。您打算什么意思?
杰伊·巴祖兹

1
如果您要寻找一种特定的语言来消除歧义,请使用C#。但基本上,只是一个简单的财产袋。它具有不同类型的不同命名属性。可以定义为类,哈希表,结构等。
递归

本文对此主题有一些深入的了解。特定于Java脚本,但原理可以重新应用于其他语言。
lala

Answers:


94

最好的方法是找到将这些参数组合在一起的方法。这假定并且仅在以下情况下才有效,如果您最终将获得多个“分组”参数。

例如,如果要传递矩形的规范,则可以传递x,y,宽度和高度,也可以传递包含x,y,宽度和高度的矩形对象。

在进行重构以寻找一些类似的东西时,请寻找类似的东西。如果这些论点确实无法合并,请开始查看您是否违反了单一责任原则。


4
好主意,但不好的榜样;Rectangle的构造函数必须具有4个参数。如果该方法需要2组矩形坐标/尺寸,则将更有意义。然后您可以传递2个矩形,而不是x1,x2,y1,y2 ...
Outlaw程序员,

2
很公平。就像我说的,只有当您最终拥有多个逻辑分组时,才有意义。
马修·布鲁贝克

23
+1:对于单一职责,它是所有答案中真正解决真正问题的少数评论之一。什么对象真正需要7个独立值来形成其标识。
AnthonyWJones

6
@AnthonyWJones我不同意。当前天气状况的数据可以具有更多独立值以形成其标识。
funct7

107

我要假设你的意思是C#。其中一些东西也适用于其他语言。

您有几种选择:

从构造器转换为属性设置器。这可以使代码更具可读性,因为对于读者而言,显而易见哪个值对应哪个参数。对象初始化器语法使它看起来不错。它也很容易实现,因为您可以使用自动生成的属性,而无需编写构造函数。

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

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

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

生成器模式

想想之间的关系stringStringBuilder。您可以在自己的班级上获得它。我喜欢将其实现为嵌套类,因此class C具有相关的class 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));

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

  1. 寻找一起有意义的参数子集。仅仅将函数的所有参数组合在一起并不会带来太多收益。目标是进行有意义的分组。 当新类型的名称很明显时,您就会知道您做对了。

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

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

例如,也许您看到一些类似于以下的代码:

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

您可以采用minSpeedmaxSpeed参数,然后将它们放入新类型:

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>。)

如您所见,Introduce Parameter Object是一个不错的开始,但是它的真正价值在于它帮助我们发现了模型中缺少的有用类型。


4
我建议先尝试“引入参数对象”,如果找不到要创建的好参数对象,则仅回退到其他选项。
道格拉斯·里德

4
极好的答案。如果您在c#语法糖之前提到了重构说明,那么恕我直言。
rpattabi 2010年

10
哦!+1表示“当新类型的名称显而易见时,您就会知道它做对了。”
肖恩·麦克米伦

20

如果是构造函数,尤其是在有多个重载的变体的情况下,则应查看Builder模式:

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

如果是正常方法,则应考虑要传递的值之间的关系,并可能创建一个传输对象。


优秀的答复。也许比每个人(包括我)给出的“将参数放入类中”答复都更相关。
Wouter Lievens

1
使您的类可变以避免仅仅向构造函数传递过多的参数可能是一个坏主意。
Outlaw程序员

@outlaw-如果需要考虑可变性,则可以轻松实现“一次性运行”语义。但是,大量的ctor参数通常表示需要配置(或者,正如其他人所指出的那样,一类试图做太多事情)。(续)
kdgregory 2009年

尽管您可以外部化配置,但在许多情况下,这是不必要的,特别是如果它是由程序状态驱动的或对于给定程序而言是标准的(请考虑XML解析器,它可以识别名称空间,并使用其他工具&c进行验证)。
kdgregory

我喜欢构建器模式,但是我将我的不可变和可变的构建器类型分开,例如string / StringBuilder,但是我使用嵌套的类:Foo / Foo.Builder。我有一个PowerShell脚本来生成用于为简单数据类执行此操作的代码。
杰伊·巴祖兹

10

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

例如:

driver.connect(host, user, pass)

你可以用

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

青年汽车


5
我肯定会更喜欢第一段代码。我同意,有一个特定的限制,在这个限制之上,numbe rof参数变得很丑陋,但是就我的口味而言,3是可以接受的。
blabla999

10

这是从福勒和贝克的书中引用的:“重构”

长参数列表

在我们早期的编程时代,我们被教导将例程所需的所有内容作为参数传递。这是可以理解的,因为替代方案是全局数据,而全局数据是邪恶的,通常是痛苦的。对象会改变这种情况,因为如果您没有所需的东西,则始终可以要求另一个对象为您获取它。因此,对于对象,您不会传递方法需要的所有内容。相反,您传递了足够的内容,以便该方法可以满足所需的一切。方法的宿主类上有很多方法需要的东西。在面向对象的程序中,参数列表往往比传统程序小得多。这很好,因为难以理解长参数列表,因为它们变得不一致且难以使用,并且因为您需要更多数据,因此您将永远对其进行更改。大多数更改都通过传递对象来删除,因为您更有可能只需要执行几个请求就可以获取新数据。如果可以通过请求已经知道的对象来获取一个参数中的数据,请使用“用方法替换参数”。该对象可能是一个字段,也可能是另一个参数。使用“保留整个对象”可从对象中收集一堆数据,并将其替换为对象本身。如果您有多个没有逻辑对象的数据项,请使用“引入参数对象”。进行这些更改有一个重要的例外。这是在您明确不想创建从被调用对象到较大对象的依赖项时。在那些情况下,解压缩数据并将其作为参数发送是合理的,但要注意所涉及的痛苦。如果参数列表太长或更改频率太高,则需要重新考虑依赖关系结构。


7

我不想听起来像个聪明人,但您还应该检查以确保传递的数据确实应该传递:将东西传递给构造函数(或该方法)闻起来有点像很少强调对象的行为

不要误会我的意思:方法和构造有很多的参数的时候。但是遇到这种情况时,请尝试考虑使用行为封装数据

对于具有很多(读取:任何)属性或吸气剂/设置剂的对象,也可能会检测到这种气味(因为我们正在谈论重构,所以这个可怕的词似乎是适当的……)。


7

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

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”)之类的信息。


5

如果某些构造函数参数是可选的,则使用生成器是有意义的,该生成器将在构造函数中获取所需的参数,并具有可选参数的方法(返回生成器),如下所示使用:

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

有关详细信息,请参见有效Java,第2版,第9页。11.对于方法参数,同一本书(第189页)描述了缩短参数列表的三种方法:

  • 将方法分解为采用较少参数的多个方法
  • 创建静态助手成员类来表示参数组,即传递a DinoDonkey代替dinoanddonkey
  • 如果参数是可选的,则可以使用上面的构建器作为方法,为所有参数定义一个对象,设置所需的参数,然后在其上调用一些execute方法

4

我将使用默认的构造函数和属性设置器。C#3.0具有一些不错的语法来自动执行此操作。

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

代码改进来自简化构造函数,而不必支持多种方法来支持各种组合。“调用”语法仍然有些“冗长”,但与手动调用属性设置程序相比,还算不错。


2
这将允许对象t new Shniz()存在。一个好的OO实现将设法使对象存在不完整状态的可能性最小化。
AnthonyWJones

通常,任何具有本机哈希/字典语法的语言都可以适当替换命名参数(这很好,通常是这些情况所需要的,但出于某种原因,唯一支持它们的流行语言是地球上最糟糕的语言) 。
混乱

4

您没有提供足够的信息来保证良好的答案。较长的参数列表并非天生就不好。

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) { ...

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

// 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) { ...

这是完全不同的情况,因为所有对象都是不同的(并且不太可能混淆)。同意如果所有对象都是必需的并且它们都是不同的,则创建参数类几乎没有意义。

另外,某些参数是可选的吗?是否存在方法重写(相同的方法名称,但不同的方法签名?),这些详细信息对于最佳答案是至关重要的。

*财产袋也可能有用,但鉴于没有背景资料,它并不是特别好。

如您所见,这个问题有不止一个正确答案。随便你吧。



2

我通常倾向于采用structs方法-大概其中大多数参数都以某种方式相关,并且代表与您的方法相关的某些元素的状态。

如果不能将参数集变成有意义的对象,则可能是信号Shniz做得太多,并且重构应包括将方法分解为单独的关注点。


2

您可以将复杂性换成源代码行。如果方法本身做得太多(瑞士刀),请尝试通过创建另一种方法将其任务减半。如果该方法很简单,只是需要太多参数,那么所谓的参数对象就是解决之道。


2

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


1

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


1

不立即在构造函数中一次设置它,而是通过属性/设置器来设置它呢?我已经看到一些利用这种方法的.NET类,例如Processclass:

        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();

3
C#3实际上具有一种轻松完成此操作的语法:对象初始化程序。
达伦·托马斯

1

我同意将参数移入参数对象(结构)的方法。但是,不要仅仅将它们全部粘贴在一个对象中,而是查看其他函数是否使用相似的参数组。如果参数对象与多个功能一起使用,则您希望该参数集在这些功能之间一致地变化,那么它就更有价值。可能是您仅将一些参数放入了新的参数对象。


1

如果您有太多参数,则该方法可能会执行过多操作,因此请先将方法拆分为几个较小的方法来解决。如果此后您仍然有太多参数,请尝试对参数进行分组或将某些参数转换为实例成员。

小班/方法优先于大班/方法。记住单一责任原则。


实例成员和属性的问题在于它们1)必须是可写的,2)可能未设置。对于构造函数,在允许实例存在之前,我要确保填充某些字段。
递归

@recursive-我不同意字段/属性始终必须是可写的。对于小班,很多时候只读成员才有意义。
布赖恩·拉斯穆森

1

命名参数是消除长(甚至短!)参数列表歧义的好选择(假设支持该语言的语言),同时也允许(对于构造函数而言)类的属性是不可变的,而无需强制允许存在处于部分构造状态。

在进行这种重构时,我会寻找的另一种选择是相关参数组,这些参数作为独立对象可能会更好。以较早答案中的Rectangle类为例,使用x,y,height和width参数的构造函数可以将x和y分解为Point对象,从而允许您将三个参数传递给Rectangle的构造函数。或者更进一步,使其成为两个参数(UpperLeftPoint,LowerRightPoint),但这将是一个更彻底的重构。


0

这取决于您拥有哪种参数,但是如果它们有很多布尔值/选项,也许您可​​以使用Flag Enum?


0

我认为该问题与您要在课堂上解决的问题的领域紧密相关。

在某些情况下,由7参数构成的构造函数可能表示不良的类层次结构:在这种情况下,上面建议的辅助struct / class通常是一种好方法,但是您最终还会倾向于装载仅是属性包的结构并且不要做任何有用的事情。由8个参数组成的构造函数还可能表明您的类太通用/太通用了,因此它需要很多选项才能真正有用。在那种情况下,您可以重构类或实现隐藏真正复杂构造函数的静态构造函数:例如。Shniz.NewBaz(foo,bar)实际上可以调用传递正确参数的真正构造函数。


0

一个考虑因素是,一旦创建对象,哪个值将是只读的?

施工后可能会分配可公开写入的属性。

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

在这种情况下,您可以从外部使用中隐藏构造函数,并为其提供Create函数。create函数采用真正的外部值并构造对象,然后使用仅库可用的访问器来完成对象的创建。

拥有一个需要7个或更多参数才能赋予该对象完整状态并且实际上完全是外部属性的对象真的很奇怪。


0

当clas的构造函数接受过多的参数时,通常表明它承担的责任过多。可以将其分解为相互协作以提供相同功能的单独类。

如果您确实需要构造函数的许多参数,则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();
    }

}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.