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

时间:2009-01-13 16:07:41

标签: refactoring

偶尔我会遇到一些参数不舒服的方法。通常情况下,他们似乎是建设者。似乎应该有更好的方式,但我看不出它是什么。

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

所以这似乎不是一种改进。那么最好的方法是什么?

24 个答案:

答案 0 :(得分:95)

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

您有几种选择:

从构造函数切换到属性setter 。这可以使代码更具可读性,因为对于读者来说,哪个值对应于哪些参数是显而易见的。 Object Initializer语法使这看起来很漂亮。它实现起来也很简单,因为您只需使用自动生成的属性并跳过编写构造函数。

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

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

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

构建器模式

考虑stringStringBuilder之间的关系。你可以为自己的课程获得这个。我喜欢将它作为嵌套类实现,因此类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类允许我扩展主类和构建器,而无需修改生成的代码。

“介绍参数对象”重构。请参阅Refactoring Catalog。我们的想法是,您将一些您传递的参数放入一个新类型,然后传递该类型的实例。如果你不假思索地这样做,你将最终回到你开始的地方:

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

变为

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

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

  1. 查找有意义的参数的子集。只是盲目地将一个函数的所有参数分组在一起并不会让你感到太多;目标是让分组有意义。 当新类型的名称显而易见时,你会知道你做对了。

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

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

  4. 例如,您可能会看到一些看起来像的代码:

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

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

答案 1 :(得分:89)

最好的方法是找到将参数组合在一起的方法。这假定,并且只有在你最终会有多个“分组”参数时才会起作用。

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

在重构时稍微清理它,看看这样的事情。如果参数确实无法合并,请开始查看您是否违反了单一责任原则。

答案 2 :(得分:20)

如果它是构造函数,特别是如果存在多个重载变体,则应该查看Builder模式:

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

如果这是一种常规方法,您应该考虑传递的值之间的关系,并且可能创建一个传输对象。

答案 3 :(得分:10)

引自福勒和贝克的书:“重构”

  

长参数列表

     

在我们早期的编程日,我们被教导要传递所需的一切参数   例程。这是可以理解的,因为替代方案是全球数据,而全球数据则是   邪恶,通常是痛苦的。对象改变了这种情况,因为如果你没有东西   你需要,你可以随时问另一个物体为你得到它。因此,对象没有   传递方法所需的一切;相反,你传递足够的方法,以便方法可以达到   它需要的一切。方法的主机类提供了许多方法所需的内容。在   面向对象的程序参数列表往往比传统的小得多   程式。   这很好,因为长参数列表很难理解,因为它们变得很难   不一致和难以使用,因为你永远根据需要改变它们   更多数据。通过传递对象可以删除大多数更改,因为您更有可能   需要只做几个请求来获取新的数据。   当您可以通过制作在一个参数中获取数据时,请使用“将参数替换为方法”   您已经知道的对象的请求。此对象可能是字段,也可能是字段   另一个参数。使用“保留整个对象”来获取从中收集的大量数据   对象并将其替换为对象本身。如果您有几个没有逻辑的数据项   对象,使用介绍参数对象。   进行这些更改有一个重要的例外。这是你明确做的时候   不想创建从被调用对象到较大对象的依赖关系。在那些情况下   拆包数据并将其作为参数发送是合理的,但要注意疼痛   参与其中。如果参数列表太长或更改太频繁,您需要重新考虑您的   依赖结构。

答案 4 :(得分:9)

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

E.g。而不是:

driver.connect(host, user, pass)

您可以使用

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

YMMV

答案 5 :(得分:7)

我不想听起来像一个明智的破解,但你也应该检查以确保你传递的数据真的应该传递:将东西传递给构造函数(或对于那个问题的方法)有点像对某个对象的行为的强调。

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

这种气味(因为我们正在谈论重构,这个可怕的词似乎合适......)也可能被检测到具有很多(读取:任何)属性或getter / setter的对象。

答案 6 :(得分: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”等等。

答案 7 :(得分:5)

如果某些构造函数参数是可选的,那么使用构建器可以获得所需参数的构建器是有意义的,并且为可选构件提供方法,返回构建器,如下所示:

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

有关详细信息,请参阅Effective Java,2nd Ed。,p。 11.对于方法参数,同一本书(p.189)描述了三种缩短参数列表的方法:

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

答案 8 :(得分:4)

我会使用默认的构造函数和属性settors。 C#3.0有一些很好的语法可以自动完成。

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

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

答案 9 :(得分:4)

您没有提供足够的信息来保证答案。长参数列表本身并不坏。

  

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

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

此外,一些参数是可选的吗?是否有方法覆盖(相同的方法名称,但不同的方法签名?)这些类型的细节都与最佳答案是什么有关。

*属性包也很有用,但没有特别好,因为没有给出背景。

正如您所看到的,这个问题有1个以上的正确答案。随便挑选。

答案 10 :(得分:3)

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

答案 11 :(得分:2)

我通常会倾向于结构方法 - 大概这些参数中的大部分都以某种方式相关,并且表示与您的方法相关的某些元素的状态。

如果参数集不能成为有意义的对象,那可能表明Shniz做得太多了,重构应该将方法分解为单独的问题。

答案 12 :(得分:2)

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

答案 13 :(得分:1)

如何不在构造函数中同时设置它,而是通过 properties / setters 来完成它?我见过一些使用这种方法的.NET类,例如Process class:

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

答案 14 :(得分:1)

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

答案 15 :(得分:1)

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

我在做这种重构时会寻找的另一个选项是相关参数组,可以作为独立对象更好地处理。使用前面答案中的Rectangle类作为示例,获取x,y,height和width参数的构造函数可以将x和y out分解为Point对象,允许您将三个参数传递给Rectangle的构造函数。或者更进一步,使它成为两个参数(UpperLeftPoint,LowerRightPoint),但这将是一个更激进的重构。

答案 16 :(得分:1)

我认为您描述的方法是可行的方法。当我找到一个包含大量参数和/或将来可能需要更多参数的方法时,我通常会创建一个ShnizParams对象来传递,就像你描述的那样。

答案 17 :(得分:1)

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

答案 18 :(得分:1)

如果你有那么多参数,很可能该方法做得太多,所以首先通过将方法分成几个较小的方法来解决这个问题。如果此后仍有太多参数,请尝试对参数进行分组或将某些参数转换为实例成员。

优先选择小班/小班。记住单一责任原则。

答案 19 :(得分:0)

简短的回答是:
您需要对相关参数进行分组重新设计我们的模型

下面的例子,构造函数接受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;
}

将相关参数分组后,
然后,构造函数将采用 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;
}

甚至让构造函数更智能,
重新设计我们的模型后
然后,构造函数将采用 ONLY 2 个参数

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

答案 20 :(得分: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();
    }

}

答案 21 :(得分:0)

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

可以在施工后指定公开可写的属性。

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

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

拥有一个需要7个或更多参数的对象才能给对象一个完整的状态并且所有这些都是真正的外部属性真的很奇怪。

答案 22 :(得分:0)

我认为这个问题与您尝试使用该课程解决的问题的范围密切相关。

在某些情况下,7参数构造函数可能表示一个错误的类层次结构:在这种情况下,上面建议的辅助结构/类通常是一个很好的方法,但是你也倾向于最终得到大量的结构只是属性袋,并没有做任何有用的事情。 8参数构造函数也可能表明您的类太通用/太通用,因此它需要很多选项才能真正有用。在这种情况下,您可以重构类或实现隐藏真正复杂构造函数的静态构造函数:例如。 Shniz.NewBaz(foo,bar)实际上可以调用真正的构造函数来传递正确的参数。

答案 23 :(得分:0)

这取决于你有什么样的参数,但是如果它们是很多布尔值/选项,你可以使用Flag Enum吗?