【问题标题】:What's the best way to refactor a method that has too many (6+) parameters?重构具有太多(6+)参数的方法的最佳方法是什么?
【发布时间】:2010-10-01 04:10:35
【问题描述】:

偶尔我会遇到参数数量令人不安的方法。通常,它们似乎是构造函数。似乎应该有更好的方法,但我看不出它是什么。

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#。但基本上,只是一个简单的属性包。它具有不同类型的不同命名属性。可以定义为类、哈希表、结构或其他任何东西。
  • This article 对该主题有一些很好的见解。特定于 Javascript,但原则可以重新应用于其他语言。

标签: refactoring


【解决方案1】:

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

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

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

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

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

【讨论】:

    【解决方案2】:

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

    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”等内容。

    【讨论】:

      【解决方案3】:

      我假设您的意思是 C#。其中一些内容也适用于其他语言。

      您有多种选择:

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

      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 classes 让我在不修改生成代码的情况下扩展主类和构建器。

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

      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&lt;Speed&gt;。)

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

      【讨论】:

      • 我建议先尝试“引入参数对象”,如果找不到要创建的好参数对象,请仅使用其他选项。
      • 优秀的答案。如果您在 c# 语法糖之前提到了重构解释,那么恕我直言,这将被评为更高。
      • 哦! +1 表示“当新类型的名称显而易见时,您就会知道自己做对了。”
      【解决方案4】:

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

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

      【讨论】:

        【解决方案5】:

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

        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 个。任君挑选。

        【讨论】:

          【解决方案6】:

          如果你有这么多参数,很可能是方法做的太多了,所以首先通过将方法拆分为几个较小的方法来解决这个问题。如果在此之后您仍然有太多参数,请尝试将参数分组或将一些参数转换为实例成员。

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

          【讨论】:

          • 实例成员和属性的问题在于它们 1) 必须是可写的,2) 可能无法设置。在构造函数的情况下,我想确保在允许实例存在之前填充某些字段。
          • @recursive - 我不同意字段/属性总是必须是可写的。对于小班,有很多时候只读成员是有意义的。
          【解决方案7】:

          我同意将参数移动到参数对象(结构)中的方法。与其将它们全部放在一个对象中,不如查看其他函数是否使用类似的参数组。如果参数对象与多个函数一起使用,则参数对象更有价值,您希望该组参数在这些函数中一致地更改。可能是你只把部分参数放到了新的参数对象中。

          【讨论】:

            【解决方案8】:

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

            【讨论】:

              【解决方案9】:

              这引自 Fowler 和 Beck 的书:“重构”

              长参数列表

              在我们早期的编程时代,我们被教导将所有需要的东西作为参数传入 例行公事。这是可以理解的,因为替代方案是全局数据,而全局数据是 邪恶的,通常是痛苦的。对象改变了这种情况,因为如果你没有东西 您需要,您可以随时要求另一个对象为您获取它。因此对于你没有的对象 传入方法所需的一切;相反,您传递了足够的信息,以便该方法可以到达 它需要的一切。方法所需的很多东西都可以在方法的宿主类中找到。在 面向对象程序的参数列表往往比传统程序的参数列表小得多 程式。 这很好,因为长参数列表很难理解,因为它们变成 不一致且难以使用,并且因为您会根据需要永远更改它们 更多数据。大多数更改都是通过传递对象来删除的,因为您更有可能 只需提出几个请求即可获取新数据。 当您可以通过以下方式获取一个参数中的数据时,请使用 Replace Parameter with Method 您已经知道的对象的请求。这个对象可能是一个字段,也可能是 另一个参数。使用 Preserve Whole Object 从 对象并将其替换为对象本身。如果您有多个没有逻辑的数据项 对象,使用引入参数对象。 进行这些更改有一个重要的例外。这是当你明确地做 不想创建从被调用对象到更大对象的依赖关系。在那些情况下 解包数据并作为参数发送是合理的,但要注意痛苦 涉及。如果参数列表太长或更改太频繁,您需要重新考虑您的 依赖结构。

              【讨论】:

                【解决方案10】:

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

                如果你真的需要构造函数的参数这么多,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();
                    }
                
                }
                

                【讨论】:

                  【解决方案11】:

                  如果某些构造函数参数是可选的,那么使用构建器是有意义的,它会在构造函数中获取所需的参数,并为可选参数提供方法,返回构建器,这样使用:

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

                  Effective Java, 2nd Ed., p. 中描述了这方面的细节。 11. 对于方法参数,同一本书 (p. 189) 描述了缩短参数列表的三种方法:

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

                  【讨论】:

                    【解决方案12】:

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

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

                    价值观最终来自哪里?也许有些值是真正的外部值,而另一些值则来自库维护的某些配置或全局数据。

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

                    如果有一个对象需要 7 个或更多参数来赋予对象一个完整的状态并且所有这些参数实际上都是外部的,那真是太奇怪了。

                    【讨论】:

                      【解决方案13】:

                      我认为这个问题与您试图通过课堂解决的问题的领域密切相关。

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

                      【讨论】:

                        【解决方案14】:

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

                        不要误会我的意思:方法和构造函数有时会有很多参数。但遇到时,请尝试考虑用 behavior 封装 data

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

                        【讨论】:

                          【解决方案15】:

                          我会使用默认的构造函数和属性设置器。 C# 3.0 有一些很好的语法可以自动执行此操作。

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

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

                          【讨论】:

                          • 这将允许对象 t new Shniz() 存在。一个好的 OO 实现会尽量减少对象存在不完整状态的可能性。
                          • 一般来说,任何具有本机哈希/字典语法的语言都可以充分替代命名参数(这很好,通常是这些情况所需要的,但由于某种原因,这是唯一支持它们的流行语言是地球上最糟糕的)。
                          【解决方案16】:

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

                          【讨论】:

                            【解决方案17】:

                            这取决于你有什么样的参数,但如果它们是很多布尔值/选项,也许你可以使用标志枚举?

                            【讨论】:

                              【解决方案18】:

                              不要在构造函数中一次性设置,而是通过 properties/setters 进行设置如何?我见过一些使用这种方法的 .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();
                              

                              【讨论】:

                              • C# 3 实际上有一个语法可以轻松做到这一点:对象初始化器。
                              【解决方案19】:

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

                              例如而不是:

                              driver.connect(host, user, pass)
                              

                              你可以使用

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

                              YMMV

                              【讨论】:

                              • 我肯定更喜欢第一段代码。我同意,有一个特定的限制,超过这个限制 rof 参数的数量就会变得丑陋,但根据我的口味,3 是可以接受的。
                              【解决方案20】:

                              如果是构造函数,特别是如果有多个重载变体,你应该看看 Builder 模式:

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

                              如果是普通方法,你应该考虑被传递的值之间的关系,并可能创建一个Transfer Object。

                              【讨论】:

                              • 优秀的回复。也许比每个人(包括我)给出的“把参数放在一个类中”的回复更相关。
                              • 为了避免向构造函数传递太多参数而使你的类可变可能是个坏主意。
                              • @outlaw - 如果关注可变性,您可以轻松实现“运行一次”语义。然而,大量的 ctor 参数通常表明需要配置(或者,正如其他人所指出的,一个试图做太多事情的类)。 (续)
                              • 虽然您可以将配置外部化,但在许多情况下这是不必要的,特别是如果它由程序状态驱动或者是给定程序的标准配置(想想 XML 解析器,它可以感知命名空间,使用不同的验证工具等)。
                              • 我喜欢构建器模式,但我将不可变和可变构建器类型分开,例如 string/StringBuilder,但我使用嵌套类:Foo / Foo.Builder。我有一个 PowerShell 脚本来为简单的数据类生成代码。
                              【解决方案21】:

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

                              如果不能将参数集变成有意义的对象,这可能表明Shniz 做得太多,重构应该涉及将方法分解为单独的关注点。

                              【讨论】:

                                【解决方案22】:

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

                                【讨论】:

                                  【解决方案23】:

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

                                  【讨论】:

                                    【解决方案24】:

                                    最好的方法是找到将参数组合在一起的方法。这是假设并且实际上仅在您最终会得到多个参数“分组”的情况下才有效。

                                    例如,如果您要传递一个矩形的规范,您可以传递 x、y、宽度和高度,或者您可以只传递一个包含 x、y、宽度和高度的矩形对象。

                                    在重构时寻找类似的东西来清理它。如果这些论点实在无法结合,那就开始看看你是否违反了单一职责原则。

                                    【讨论】:

                                    • 好主意但不好的例子; Rectangle 的构造函数必须有 4 个参数。如果该方法需要 2 组矩形坐标/尺寸,这将更有意义。然后你可以传递 2 个矩形而不是 x1, x2, y1, y2...
                                    • 很公平。就像我说的,只有当你最终得到多个逻辑分组时才有意义。
                                    • +1:对于单一责任,它是所有真正解决真正问题的答案中为数不多的几个 cmets 之一。什么对象真正需要 7 个独立的值来形成它的身份。
                                    • @AnthonyWJones 我不同意。当前天气状况的数据可以有更多独立的值来形成其身份。
                                    猜你喜欢
                                    • 1970-01-01
                                    • 1970-01-01
                                    • 1970-01-01
                                    • 1970-01-01
                                    • 1970-01-01
                                    • 2015-04-04
                                    • 1970-01-01
                                    • 1970-01-01
                                    • 2021-03-28
                                    相关资源
                                    最近更新 更多