Bulider Design Pattern to make generic method for methods having large number of parameters

前端 未结 4 1336
清歌不尽
清歌不尽 2020-12-21 09:39

I have an interface Itest and ClassA & ClassB are implementing this interface. testA & testB are met

4条回答
  •  孤城傲影
    2020-12-21 10:10

    Parameter Object through Builder-Pattern

    First and foremost, The builder pattern is kind of an instance factory where you get a simple POJO as a result after calling .build() or something similar to that on the builders instance.

    The builder therefore follows often this kind of syntax:

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

    This pattern often goes hand in hand with a limited scope (private or protected) of the constructor of the concrete object, but does not insist on that.

    Although Timo has already given an example where you can use a combination of the Parameter Object and Builder patterns, writing a builder which collects arguments already captured by an other builder before may lead to plenty of copy& paste code (don't repeat yourself).

    I've therefore come up with a parental builder setup which might be of interest to you especially if you might need to extend the generated parameter object in future.

    The core of this extensible builder pattern is an abstract TestParam class which has also an abstract builder defined.

    public abstract class TestParam
    {
        public static abstract class CommonBuilder, 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 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() +"]";
        }
    }
    

    This abstract class have all the common parameters (a, b and c) found in your example and an additional optional parameter z whose type can be passed generically. Except from the abstract definition most of the stuff should be straight forward. The definition of the generic builder type is so that we can actually create proper child classes via child builders.

    A child class (including a child builder) can now look like this:

    public class TestParamA extends TestParam
    {
        public static class Builder, B extends TestParamA.Builder, ? extends B, D,E,Z>, D,E,Z> extends TestParam.CommonBuilder, 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 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;
        }
    }
    

    Here most of the stuff is quite simple except for the generic type definition:

    Builder, 
            B extends TestParamA.Builder, ? extends B, D,E,Z>, 
            D,E,Z> 
        extends TestParam.CommonBuilder, Z>
    
    • T is the type of the object to create via the builder (TestParamA, TestParamB, ...)
    • B is the current instance of the builder which creates the parameter object. This looks rather complicated but gurantees that the child-builder is used and not fallback to the parent-builder if you use a method from the parent-builder.
    • D, E, Z are the actual types of the parameters passed to the builder

    I don't post TestParamB here as this is almost identical to TestParamA except that it defines builder-operations withF(...) and withG(...) instead of withD(...) and withE(...) and also prints the F and G equivalent output.

    You have now a couple of options to use your builder in conjunction with method declarations. As I'm not sure which method is best suited for you, I've created a small test-case with multiple different invocations:

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

    On running this test class the following output should be returned:

    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() and the other classes created with new are just simple POJOs which return their simple class name in toString().

    As can be seen each invoked test-method contains the appropriate child parameter object created through the corresponding builder. For more general methods like test(TestParam testParam) or testCommon(...) you might need to cast the parameter object to the concrete class before actually gaining access to those methods (getD(), ...) unique to the concrete classes - but I guess you are familiar with that concept anyway.

    CONS

    • Writing a builder creates additional overhead compared to a traditional constructor call
    • Creating a new instance also comes with additional costs of extra characters to type

    PROS

    • Flexible order of parameters possible. Usually you don't have to remember the order of parameters, which is quite nice if you deal with more than 5+ parameters. Required parameters, though, are often specified within the constructor of the builder and therefore require a fixed order, unless they can be specified using builder-methods.
    • Supports grouping of related parameters (like .dimensions(int x, int y, int width, int height))
    • Type safty
    • Extensibility (as showcased in this post)
    • Generated types can be used as Parameter Objects and thus rely on polymorphism if the created objects follow a parent-child structure
    • Increased readability support. Although argued in the comments of this post, builders increase readability if you return to the code month later and have to remember what all those parameters passed were. Builders add some kind of lexical semantic to the parameters. Readability can be thus increased by structuring the fluent method calls appropriately

    When (not) to use Builders

    That being said, builders are nice but also come with overhead. You should not use them if there are only few parameters or many different independent types should be created as a builder needs to be set up for each type. Here a simple POJO instantiation for the first case and a general factory pattern for the latter case is superior IMO.

    If your approach needs to be as flexible as possible and you don't need to rely on type safety or provide some internal type extraction mechanism (like Camel's type converters), use Map as parameter object instead. Camel uses this approach for its message headers. Also the Activiti BPMN engine uses this approach. (explained by AdamSkywalker in this thread)

    If you have a limited number of scenarios and a clear number of parameters, use simple method overloading (as explained by Chetan Kinger).

    If you struggle with remembering the exact order of parameters over time, there might be some kind of class extension going on in the future or if you have a bunch of optional parameters (maybe even with some default values) builders come in nicely.

提交回复
热议问题