代码之家  ›  专栏  ›  技术社区  ›  recursive

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

  •  84
  • recursive  · 技术社区  · 16 年前

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

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

    所以这似乎不是什么进步。那么最好的方法是什么呢?

    23 回复  |  直到 14 年前
        1
  •  87
  •   Matthew Brubaker    16 年前

    最好的方法是找到将论点分组的方法。这是假设的,并且只在您最终得到多个“分组”参数时才有效。

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

    在重构时查找类似的内容,以便对其进行一定程度的清理。如果这些论点真的不能结合在一起,那么就开始研究你是否违反了单一责任原则。

        2
  •  94
  •   Jay Bazuzi Buck Hodges    15 年前

    我想你是说 C.* . 其中一些东西也适用于其他语言。

    您有几个选项:

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

    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 类允许我扩展主类和生成器,而不修改生成的代码。

    “引入参数对象”重构 . 见 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;
    }
    

    你可以拿着 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<Speed> )

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

        3
  •  20
  •   kdgregory    16 年前

    如果它是一个构造函数,特别是在有多个重载变量的情况下,您应该查看构建器模式:

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

    如果这是一个普通的方法,您应该考虑正在传递的值之间的关系,并可能创建一个传输对象。

        4
  •  10
  •   Youssef    16 年前

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

    长参数列表

    在我们早期的编程时代,我们被教导作为参数传递 例行公事这是可以理解的,因为备选方案是全球数据,而全球数据是 邪恶,通常是痛苦的。对象会改变这种情况,因为如果您没有 你需要的,你可以随时要求另一个对象为你得到它。所以对于你不喜欢的东西 传递方法所需的所有内容;相反,您传递的内容足够多,以便方法能够 它所需要的一切。方法的宿主类上有很多方法需要的内容。在 面向对象程序的参数列表往往比传统的要小得多。 程序。 这很好,因为很难理解长的参数列表,因为它们 不一致和难以使用,因为你一直在根据需要改变它们 更多的数据。大多数更改都是通过传递对象删除的,因为 只需要发出几个请求就可以得到一个新的数据。 当可以通过 对您已经知道的对象的请求。此对象可能是字段,也可能是 另一个参数。使用“保留整个对象”从 对象并将其替换为对象本身。如果有几个数据项没有逻辑 对象,使用引入参数对象。 进行这些更改有一个重要的例外。这是你明确的时候 不希望创建从被调用对象到较大对象的依赖关系。在那些情况下 解包数据并将其作为参数发送是合理的,但要注意痛苦 卷入的。如果参数列表太长或更改太频繁,则需要重新考虑 依赖关系结构。

        5
  •  9
  •   Wouter Lievens    16 年前

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

    例如,代替:

    driver.connect(host, user, pass)
    

    你可以使用

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

    牛传染性胃肠炎病毒

        6
  •  7
  •   Daren Thomas    16 年前

    我不想听起来像个聪明的裂缝,但你也应该检查一下,确保你传递的数据是正确的。 真的? 应该传递给:将东西传递给构造函数(或方法)闻起来有点像对 行为 物体的

    别误会我:方法和构造函数 有时参数很多。但遇到这种情况时,一定要考虑封装 数据 具有 行为 相反。

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

        7
  •  7
  •   Jay    14 年前

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

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

        8
  •  5
  •   Fabian Steeg    16 年前

    如果一些构造函数参数是可选的,那么使用一个生成器是有意义的,它将在构造函数中获得所需的参数,并为可选参数提供方法,返回生成器,如下所示:

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

    这方面的细节在有效的Java,第二版,第11页中进行了描述。对于方法参数,同一本书(第189页)描述了缩短参数列表的三种方法:

    • 将该方法分解为多个使用较少参数的方法
    • 创建静态助手成员类以表示参数组,即传递 DinoDonkey 而不是 dino donkey
    • 如果参数是可选的,那么方法可以采用上面的生成器,为所有参数定义一个对象,设置所需的参数,然后对其调用一些执行方法。
        9
  •  4
  •   tvanfosson    16 年前

    我将使用默认的构造函数和属性设置器。C 3.0有一些很好的语法来自动完成这项工作。

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

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

        10
  •  4
  •   Robert Paulson    16 年前

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

    希尼兹(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) { ...
    

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

    另外,一些参数是可选的吗?是否存在方法重写(相同的方法名,但不同的方法签名?)这些细节都关系到 最好的 答案是。

    *属性包也可能有用,但考虑到没有给定背景,这并不是特别好。

    正如你所看到的,这个问题有一个以上的正确答案。你挑吧。

        11
  •  3
  •   Julien Hoarau    16 年前

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

        12
  •  2
  •   Andrzej Doyle    16 年前

    我通常倾向于结构方法——假设这些参数中的大多数以某种方式相关,并且表示与您的方法相关的某个元素的状态。

    如果参数集不能成为一个有意义的对象,这可能是一个标志 Shniz 做的太多了,重构应该包括将方法分解为单独的关注点。

        13
  •  2
  •   user54650    16 年前

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

        14
  •  1
  •   Instantsoup    16 年前

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

        15
  •  1
  •   Gant    16 年前

    不在构造函数中同时设置,而是通过 属性/设置器 ?我见过一些使用这种方法的.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();
    
        16
  •  1
  •   Karl    16 年前

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

        17
  •  1
  •   Frank Schwieterman    16 年前

    我同意将参数移动到参数对象(结构)中的方法。不过,与其将它们全部粘贴在一个对象中,还不如检查其他函数是否使用类似的参数组。如果一个参数对象与多个函数一起使用,而您希望这些函数中的参数集一致地发生更改,那么它就更有价值。可能只是将一些参数放入新的参数对象中。

        18
  •  1
  •   Brian Rasmussen    16 年前

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

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

        19
  •  1
  •   Dave Sherohman    16 年前

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

    在进行此类重构时,我要寻找的另一个选项是相关参数组,它们可以更好地作为独立对象进行处理。以前面答案中的rectangle类为例,采用x、y、height和width参数的构造函数可以将x和y分解为一个点对象,从而允许您将三个参数传递给矩形的构造函数。或者更进一步,使其成为两个参数(upperLeftPoint、lowerRightPoint),但这将是一个更激进的重构。

        20
  •  0
  •   scottm    16 年前

    这取决于您有什么类型的参数,但是如果它们是许多布尔值/选项,那么您可以使用标志枚举吗?

        21
  •  0
  •   axel_c    16 年前

    我认为这个问题与你想用课堂解决的问题有着密切的联系。

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

        22
  •  0
  •   AnthonyWJones    16 年前

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

    公共可写属性可能在构造之后被分配。

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

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

    如果一个对象需要7个或更多的参数来给它一个完整的状态,并且所有的参数都是真实的外部属性,那就真的很奇怪了。

        23
  •  0
  •   Guillaume    16 年前

    当一个CLA有一个接受了太多参数的构造函数时,它通常是一个信号,表明它有太多的职责。它可能被分成不同的类,这些类相互协作以提供相同的功能。

    如果您真的需要这么多的构造函数参数,那么构建器模式可以帮助您。目标仍然是将所有参数传递给构造函数,因此从一开始就初始化了它的状态,如果需要,还可以使类不可变。

    见下文:

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