【问题标题】:Bulider Design Pattern to make generic method for methods having large number of parameters构建器设计模式为具有大量参数的方法创建通用方法
【发布时间】:2015-06-04 09:23:24
【问题描述】:

我有一个接口ItestClassA & ClassB 正在实现这个接口。 testA & testB 分别是这些类中的方法。

testA(String a, String b, String c, D d, E e)

testB(String a, String b, String c, F f, G g) 

这里的DEFG 是自定义数据类型(与数据库相关)。我简化了方法,实际上它们有更多的参数。

我需要在Itest 接口中的testAB 中创建一个通用方法,并在两个类中实现它,而不是拥有自己的方法。

testAB(String a, String b, String c, D d, E e, F f, G g)

随着参数的数量越来越多,泛型方法testAB 对用户来说会很痛苦,因为他必须传递这么多null 值。

  • 这是 Bulider 设计模式的用例吗?

  • 如果是,如何使用这种设计模式实现这一点?

【问题讨论】:

  • 以后有可能有ClassX和ClassY自带参数吗?
  • @AdamSkywalker 没有找到你。 X 类?
  • 我的意思是你的例子中有两个类。也许将来你会有四门课或更多课?
  • 是的,未来可能会有所不同。
  • 你能按逻辑方式对DEFG 进行分组吗?例如,如果您有 testA(a, b, databaseHost, databasePort, username, password, databaseName),您可以将其更改为 testA(a, b, ConnectionParameters) 并让 ConnectionParameters 包含 usernamepassworddatabaseHostdatabasePortdatabaseName

标签: java design-patterns


【解决方案1】:

看起来您的核心要求是您不希望客户端在不需要时传递其他参数。您可以使用简单的旧方法overloading 来解决您的问题:

更改您的ITest 接口,使其具有一个名为test 的方法

public interface ITest {
     public void test(String a,String b,String c,D d,E e,F f,G g);
}

修改A如下:

public class A implements ITest {

     //this is an overload - v1
     public void test(String a,String b,String c,D d,E e) {
            //dispatch the call to the overriden method
            test(a,b,c,d,e,null,null);
     }

     //this is an overload - v2
     public void test(String a,String b,String c,E e,F f) {
           //dispatch the call to the overriden method
           test(a,b,c,null,null,e,f);
     }

     @Override
     //this is an overriden method - v3
     public void test(String a,String b,String c,D d,E e,F f,G g) {
            if(d!=null && e!=null) {
                //use a,b,c,d,e and do something
            } 

            if(f!=null && g!=null) {
                //use a,b,c,f,g and do something
            }
     }
}

现在客户端代码可以调用他们想要的任何重载形式,而无需传递null。您的重载方法将简单地将调用分派给一个通用方法(这为您提供了代码重用的优势):

classAObj.test("1","2","3",new D(),new E());//calls overloaded method - v1
classAObj.test("1","2","3",new F(),new G());//calls overloaded method - v2
classAObj.test("1","2","3",new D(),new E(),new F(),new G());//calls overriden method - v3

请注意客户端代码如何不必担心在不需要时传递额外的参数。还要注意客户端调用看起来多么干净。 B 也可以进行类似的更改。


1。您可以选择将ITest 设为抽象类。这将允许您使其中的test 方法具有protected 访问说明符。需要protected 访问说明符的原因是限制客户端类能够访问该方法并始终通过重载表单。这是一个附加功能,如果您目前坚持使用interface,您可以考虑在未来实施。

2。您还可以利用Generics 来避免每次引入新对象类型时都需要编写新类,但正如您从其他答案中看到的那样,这很容易在很大程度上使您的代码复杂化。您还可以将重载方法添加到ITest 接口,使其成为其合同的一部分。但是,我故意将这些部分排除在我的答案之外,因为您的问题的症结可以通过使用overloading 来解决。

3。 Builder 模式是一种创建模式。在这种特殊情况下,这是一种过度杀伤力,因为诸如 DEFG 之类的类是域对象。 AB 类在真正意义上并不真正依赖它们,而是将它们用作数据源。

【讨论】:

  • 你解释得很好,但我需要处理现有的具有接口的代码。
  • @dev 我已经编辑了我的答案以使用interface 而不是abstract 类。 abstract 类的使用是可选的。另请注意,我的回答使您的代码简洁易读。乱七八糟的代码,即使是编写它的开发人员也无法理解,这是毫无意义的
  • 您声称构建器模式对于多个参数是不可读的,但尽管提出了一种方法,您可以将可以传递给方法参数集的每个参数聚集在一起 - 即使您并不真正使用它们。想象一下,您已经重载了一个方法 50 次(我们不知道 OP 的确切需求),并且您正在添加一个进一步的参数 - 您需要调整每个重载的方法以使这个附加参数无效。此外,如果两个测试具有相同类型的参数会发生什么?您还需要将逻辑纳入重载方法。
  • 此外,过去我遇到过几种情况,其中 4-5 个参数导致我不确定参数 x 是在位置 3 还是 4 还是 5,因此必须首先查看一些文档——而测试代码通常根本没有文档——与整体长参数列表相比,这是构建器模式实际上提高可读性的地方——但我想我们应该停止在这个特定问题上进一步争论。 OP 应自行决定哪种方法最适合他的需求。
  • @RomanVottner 我非常清楚Builder 模式解决的问题是避免telescoping 方法或构造函数。我也知道记住参数位置是一个很大的禁忌。这并不意味着在这种情况下您应该始终使用Builder 模式。您在我的回答中指出的所有问题都可以使用polymorphism 轻松解决。在我的回答中解释这一点会使我的回答很长。我的解决方案的关键是方法重载,这是我想在我的回答中强调的。除此之外的任何事情都是用勺子喂食的。
【解决方案2】:

是的,您可以在这里使用构建器模式。 您希望将保存信息的对象传递给您的方法。

可以使用内部构建器创建这些对象,而字段可以保存默认值。这是一个小例子。

ParamTestA<D, E> paramA = new ParamTestA<>.Builder(a, b, c).setD(d).setE(e).build();

testA(paramA);

【讨论】:

  • 您在这里使用的是Parameter Object 设计模式,您可以在其中使用构建器模式创建参数对象。
  • 嗯我不知道这样的设计模式。在这种情况下,对象更可能是一个 POJO,它使用构建器模式构建,然后 (ab) 用作参数持有者。
  • 如果您将构建器模式替换为传统的 POJO 实例化,您最终仍将拥有相同的 POJO。 Parameter Object 只是将所有方法参数重构为单个对象。这就是为什么这里使用的实际模式是参数对象 - 但这只是为了正确命名。
【解决方案3】:

感觉你在做错事:

首先你需要一个在你的界面中有很多泛型参数的方法

interface ITest<D,E,F,G> {
   void test(String a, D d, E e, F f, G g)
}

这是错误的,因为您的接口与实现细节紧密耦合

如果你会尝试从方法参数的差异中抽象出来

interface ITest {
   void test(String a, Map<String, Object> params);
}

你会得到你想要的,但你会失去泛型类型检查。

无论如何我都会向你推荐这个变体,因为你负责将参数传递给你的方法。

【讨论】:

  • 是什么阻止了客户端将Dog 对象传递给参数映射?想象一下 instanceoftest 方法中检查和强制转换的数量会因此而需要吗?示例:if(params.get("E") instanceof E) { ((E)params.get("E")).getFieldOnlyPresentInE() }
  • 我预计将参数传递给方法并在测试中接收参数的人是同一个人。显然,如果此操作不相关,此解决方案将不起作用。
【解决方案4】:

参数对象通过Builder-Pattern

首先,构建器模式是一种实例工厂,在调用 .build() 或类似构建器实例上的类似内容后,您会得到一个简单的 POJO。

因此构建器经常遵循这种语法:

SomeClass instance = new SomeClass.Builder<>(requiredArgument).optionalArgumentX(x).build();

这种模式通常与具体对象的构造函数的有限范围(privateprotected)齐头并进,但并不坚持这一点。

虽然 Timo 已经给出了一个示例,您可以在其中使用 Parameter ObjectBuilder 模式的组合,但编写一个收集之前已经被其他构建器捕获的参数的构建器可能会导致大量复制和粘贴代码(不要不要重复自己)。

因此,我提出了一个父构建器设置,您可能会感兴趣,特别是如果您将来可能需要扩展生成的参数对象。

这种可扩展构建器模式的核心是一个抽象的TestParam 类,它还定义了一个抽象构建器。

public abstract class TestParam<Z>
{
    public static abstract class CommonBuilder<T extends CommonBuilder<T, Z>, Z>
    {
        protected final String a;
        protected final String b;
        protected final String c;
        protected Z z = null;

        public CommonBuilder(String a, String b, String c) 
        {
            this.a = a;
            this.b = b;
            this.c = c;
        }

        public T withOptionalZ(Z z)
        {
            this.z = z;
            return (T)this;
        }

        public abstract <T> T build();
    }

    protected final String name;
    protected final String a;
    protected final String b;
    protected final String c;
    protected Z z = null;

    protected TestParam(String name, String a, String b, String c)
    {
        this.name = name;
        this.a = a;
        this.b = b;
        this.c = c;
    }

    protected TestParam(String name, String a, String b, String c, Z z)
    {
        this.name = name;
        this.a = a;
        this.b = b;
        this.c = c;
        this.z = z;
    }

    public String getA() 
    {
        return a;
    }

    public String getB()
    {
        return b;
    }

    public String getC()
    {
        return c;
    }

    protected abstract String getContent();

    @Override
    public String toString()
    {
        return name+"[A: " + a + ", B: " + b + ", C: " + c + (z != null ? ", Z: " + z.toString() : "") + getContent() +"]";
    }
}

这个抽象类具有您示例中的所有公共参数(abc)以及一个附加的可选参数 z,其类型可以通用传递。除了抽象定义之外,大多数东西都应该是直截了当的。通用构建器类型的定义是为了让我们实际上可以通过子构建器创建适当的子类。

子类(包括子构建器)现在可以如下所示:

public class TestParamA<D,E,Z> extends TestParam<Z>
{
    public static class Builder<T extends TestParamA<D,E,Z>, B extends TestParamA.Builder<? extends TestParamA<D,E,Z>, ? extends B, D,E,Z>, D,E,Z> extends TestParam.CommonBuilder<TestParamA.Builder<?,?, D,E,Z>, Z>
    {
        protected D d;
        protected E e;

        public Builder(String a, String b, String c)
        {
            super(a, b, c);
        }

        public B withD(D d)
        {
            this.d = d;
            return (B)this;
        }

        public B withE(E e)
        {
            this.e = e;
            return (B)this;
        }

        @Override
        public <T> T build()
        {
            TestParamA t = new TestParamA("TestParamA", a, b, c, z, d, e);
            return (T)t;
        }        
    }

    protected final D d;
    protected final E e;

    protected TestParamA(String name, String a, String b, String c, Z z, D d, E e)
    {
        super(name, a, b, c, z);
        this.d = d;
        this.e = e;
    }

    public D getD()
    {
        return d;
    }

    public E getE()
    {
        return e;
    }

    @Override
    protected String getContent()
    {
        return ", D: " + d + ", E: " + e;
    }
}

除了泛型类型定义之外,这里大部分的东西都很简单:

Builder<T extends TestParamA<D,E,Z>, 
        B extends TestParamA.Builder<? extends TestParamA<D,E,Z>, ? extends B, D,E,Z>, 
        D,E,Z> 
    extends TestParam.CommonBuilder<TestParamA.Builder<?,?, D,E,Z>, Z>
  • T 是要通过构建器创建的对象的类型(TestParamATestParamB、...)
  • B 是创建参数对象的构建器的当前实例。这看起来相当复杂,但如果您使用来自父构建器的方法,则保证使用子构建器并且不会回退到父构建器。
  • DEZ是传递给builder的参数的实际类型

我不在这里发布TestParamB,因为它几乎与TestParamA 相同,只是它定义了构建器操作withF(...)withG(...) 而不是withD(...)withE(...),并且还打印了@987654348 @ 和 G 等效输出。

您现在有几个选项可以将构建器与方法声明结合使用。由于我不确定哪种方法最适合您,因此我创建了一个包含多个不同调用的小型测试用例:

public class Main
{
    public static void main(String ... args)
    {
        TestParamA<D,E,?> a = new TestParamA.Builder<>("a","b","c").withD(new D()).withE(new E()).build();
        TestParamB<F,G,String> b = new TestParamB.Builder<>("a","b","c").withF(new F()).withG(new G()).withOptionalZ("z").build();
        TestParam<String> c = new TestParamA.Builder<>("a","b","c").withD(new D()).withE(new E()).withOptionalZ("z").build();
        TestParam d = new TestParamB.Builder<>("a","b","c").withF(new F()).withG(new G()).build();

        test(a);
        test(b);
        test(c);
        test(d);
        test(new TestParamA.Builder<>("a","b","c").withD(new D()).withE(new E()));
        test(new TestParamB.Builder<>("a","b","c").withF(new F()).withG(new G()).withOptionalZ("z"));
        testCommon(new TestParamA.Builder<>("a","b","c").withD(new D()).withE(new E()).withOptionalZ("z"));
        testCommon(new TestParamB.Builder<>("a","b","c").withF(new F()).withG(new G()));
    }

    public static void test(TestParamA<?,?,?> testParam)
    {
        System.out.println("Test for ParamA: " + testParam.toString());
    }

    public static void test(TestParamB<?,?,?> testParam)
    {
        System.out.println("Test for ParamB: " + testParam.toString());
    }

    public static void test(TestParam<?> testParam)
    {
        System.out.println("Test for Param: " + testParam.toString());
    }

    public static void test(TestParamA.Builder<?,?,?,?,?> builder)
    {
        System.out.println("Test for BuilderA: " + builder.build().toString());
    }

    public static void test(TestParamB.Builder<?,?,?,?,?> builder)
    {
        System.out.println("Test for BuilderB: " + builder.build().toString());
    }

    public static void testCommon(TestParam.CommonBuilder<?,?> builder)
    {
        System.out.println("Test for CommonBuilder: " + builder.build().toString());
    }
}

在运行这个测试类时,应该返回以下输出:

Test for ParamA: TestParamA[A: a, B: b, C: c, D: D, E: E]
Test for ParamB: TestParamB[A: a, B: b, C: c, Z: z, F: F, G: G]
Test for Param: TestParamA[A: a, B: b, C: c, Z: z, D: D, E: E]
Test for Param: TestParamB[A: a, B: b, C: c, F: F, G: G]
Test for BuilderA: TestParamA[A: a, B: b, C: c, D: D, E: E]
Test for BuilderB: TestParamB[A: a, B: b, C: c, Z: z, F: F, G: G]
Test for CommonBuilder: TestParamA[A: a, B: b, C: c, Z: z, D: D, E: E]
Test for CommonBuilder: TestParamB[A: a, B: b, C: c, F: F, G: G]

new D() 和使用new 创建的其他类只是简单的 POJO,它们在toString() 中返回它们的简单类名。

可以看出,每个调用的测试方法都包含通过相应的构建器创建的适当的子参数对象。对于更通用的方法,如test(TestParam&lt;?&gt; testParam)testCommon(...),您可能需要将参数对象强制转换为具体类,然后才能真正访问具体类独有的那些方法(getD(),...) - 但我猜你无论如何都熟悉这个概念。

缺点

  • 与传统的构造函数调用相比,编写构建器会产生额外的开销
  • 创建新实例还需要额外输入额外字符

优点

  • 参数的灵活顺序可能。通常您不必记住参数的顺序,如果您处理超过 5 个以上的参数,这非常好。但是,必需的参数通常在构建器的构造函数中指定,因此需要固定的顺序,除非它们可以使用构建器方法指定。
  • 支持对相关参数进行分组(如.dimensions(int x, int y, int width, int height)
  • 类型安全
  • 可扩展性(如本文所示)
  • 生成的类型可以用作Parameter Objects,因此如果创建的对象遵循父子结构,则依赖于多态性
  • 增加了可读性支持。尽管在这篇文章的 cmets 中有争论,但如果您在一个月后返回代码并且必须记住所有传递的参数是什么,构建者会增加可读性。构建器为参数添加了某种词汇语义。因此,可以通过适当地构造流畅的方法调用来提高可读性

何时(不)使用构建器

话虽如此,建设者很好,但也有开销。如果只有很少的参数或应该创建许多不同的独立类型,则不应使用它们,因为需要为每种类型设置构建器。在这里,第一种情况的简单 POJO 实例化和后一种情况的通用工厂模式是优越的 IMO。

如果您的方法需要尽可能灵活,并且您不需要依赖类型安全或提供一些内部类型提取机制(如 Camel 的类型转换器),请改用 Map&lt;String, Object&gt; 作为参数对象。 Camel 将这种方法用于其消息头。 Activiti BPMN 引擎也使用这种方法。 (由 AdamSkywalker 在此线程中解释)

如果您的场景数量有限且参数数量明确,请使用简单的方法重载(如 Chetan Kinger 所述)。

如果您随着时间的推移难以记住参数的确切顺序,那么将来可能会进行某种类扩展,或者如果您有一堆可选参数(甚至可能带有一些默认值),构建器会很好地出现.

【讨论】:

  • 你应该得到一个 cookie
  • 令人难以置信的努力。向你致敬。话虽如此。您提出的 API 非常不可读。例如,只有两个对象DE 的调用test(new TestParamA.Builder&lt;&gt;("a","b","c").withD(new D()).withE(new E())); 本身会让API 的用户有点头晕。想象一下当参数数量为 10 时会发生什么?客户端代码真的应该做这么多工作吗?
  • @ChetanKinger 你也可以用一个方法传递多个参数(分组参数),这个例子只是没有指定它们。你必须自己决定什么最适合你。如果您有多个参数,则构建器模式很有用,因为构建器的参数顺序非常灵活,并且与映射相比,您仍然具有编译时检查安全性。最重要的是,如果您因此设置代码样式,您将获得可读性(不像本示例中那样)
  • @RomanVottner 我不认为 OP 正在寻找需要将参数包装到对象中的解决方案(即分组参数)。此外,正如我在回答中所解释的那样,构建器模式有点矫枉过正,因为它使代码非常不可读,并使客户端代码在这种特殊情况下做很多工作。
  • @ChetanKinger 首先,OP 询问是否可以使用构建器模式(假设他知道构建器模式是什么,他可能还针对参数对象方法)。接下来,您将参数分组与参数对象本身混合在一起。构建器可能有一个方法 .withDimensions(int x, int y, int width, int height) 和一个指定颜色的方法 .withColor(Color c) - 在这里进行逻辑分组,这些参数通常是齐头并进的 - 当然所有参数本身都包含在最终对象中。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-01-27
  • 1970-01-01
  • 1970-01-01
  • 2011-03-14
  • 1970-01-01
  • 2023-01-02
相关资源
最近更新 更多