2

I am trying to identify the pros and cons of two approaches to create an object to return from my generic API. I am thinking the first approach I am sketching out has the advantage of being easier to understand by offshore developers, while the second approach lends itself better to concurrency or more complicated logic on the client side.

Example object model:

class TopLevelResponse {
    String field1;
    String field2;
    MidLevelResponse field3;
    ResponseMisc[] field4;
}

class MidLevelResponse {
    String field1;
    BottomLevelResponse field2;
}

class BottomLevelResponse {
    String field1;
    String[] field2;
}

class ResponseMisc {
    String field1;
    String field2;
}

First top-down case where each branching object is created whenever a child field is initialized:

class MyResponseInitializer {
    TopLevelResponse rsp;
    TopLevelResponse getTopLevelResponse() {
        if (Objects.isNull(rsp)) rsp = new TopLevelResponse();
        return rsp;
    }
    MidLevelResponse getMidLevelResponse() {
       if (Objects.isNull(getTopLevelResponse().getMidLevelResponse())
           getTopLevelResponse().setMidLevelResponse(new MidLevelResponse());
       return getTopLevelResponse().getMidLevelResponse();
    }
    ...
    void setField1(String val) {
        getTopLevelResponse().setField1(val);
    }
    void setField2(String val) ...
    void setMidLevelField1(String val) ...
    ...
} 

And on client side:

...
MyResponseInitializer rspInit = new MyResponseInitializer();
rspInit.setField1("foo");
rspInit.setField2("bar");
rspInit.setMidLevelField1("baz");
...
TopLevelResponse rsp = rspInit.getTopLevelResponse();

Compare to a bottom-up approach like this, where initialization is performed by a series of builders:

static class BottomLevelResponseBuilder(){
     ...
     BottomLevelResponse build()...
}
static class MidLevelResponseBuilder()
...
static class TopLevelResponseBuilder()
...

Which on client side looks like this:

TopLevelResponse rsp = TopLevelResponseBuilder.newInstance()
                           .field1("foo")
                           .field2("bar")                                             
                           .field3(MidLevelResponseBuilder.newInstance()
                                .field1("baz")...
                       ...
                       .build();

What are the pros vs cons of either approach here? Am I right to think that the first approach is better from a defensive programming perspective, since it reduces the risk of rogue implementations by offshore devs? Or that using concurrency to create each branch on a separate thread can pay off especially with more nesting involved in the data model if I use approach 2?

0

Defensive Code and Rogue implementation by offshore devs - Both the reasons should not be the criteria for choosing/not choosing builder.

When do we generally use builder? When you have a complex object to be created and not all fields are required during instantiation, rather than providing overloaded constructors (which the offshore devs can mess up while using), you provide more inferable static methods to set the fields.

Does this mean you will not provide setters for some/all fields? Not really. Depends on your application and how the values of the fields change. Will this allow for rogue implementations? Regardless of what patterns are used, rogue implementations are always possible, and this can only be caught during design/code reviews or by proactively letting the team know of what has been done for them to notice.

"Using concurrency to create each branch on a separate thread" : Is your Class that heavy that it needs multiple threads to create a single object of that class? You can relook at that actually. Anyway, even in the first case you can have setters called from each thread.

For your use case, go with the builder pattern, and if you are really concerned, you can create private setters so that they are not implemented (which again I don't prefer).

2
  • The classes I work with can get really heavy - these get converted to XML responses sometimes in megabytes with dozens of fields. Not sure where the line to call it heavy enough for concurrency would lie though. As for the defensive programming being a reason, I have had my code gutted when I tried to reduce boilerplate and I am not really in a position to take proper measures beyond coding for compromise with devs with little training paid peanuts
    – user313675
    Jan 18 '20 at 14:47
  • 1
    You are not doing yourself (or anyone) a favour by compromising much. Good code could be the starting point for the team to improve. I might not know about your team's dynamics, but at some point the effort for cleaning up mess and trying to maintain will be appreciated, and it will start getting a lot easier when the team members are on the same page. In the meanwhile, if the code is well commented and documented (maybe a little verbose to being with), it will make it easier for team adoption and things might ease out. My 2¢.
    – skott
    Jan 19 '20 at 5:11
3

I can see your point about top down vs bottom up but what you're really contrasting here is a half baked Java Bean initializer with the Joshua Bloch Builder (not to be confused with the very different GoF Builder).

Java Beans have a no arg constructor, are serializable, and have setters for each field. These were designed so persistence libraries could use reflection to figure out how to build them. They were never designed for humans. But lazy humans decided it was a workable pattern and started imitating them even when a persistence library wasn't being used.

The Joshua Bloch Builder is designed for humans and offers a lot of features that Java Beans don't have:

  • Immutable - the final object can be trusted to stay the same since it's in its final state when it's born. No need for setters.
  • Null free - the final object is never in a partially completed state because it isn't constructed until all the fields are known.
  • Self consistent - validation can easily consider the whole object, not just the single field being set.

Both patterns simulate named arguments in languages that don't have them (like Java) which humans prefer to long constructors. Both involve considerable boiler plate code to make them work. Both are reasonable to use to construct basic objects. Neither requires a complex object structure to justify them.

The half baked Beans advantage is that it's older and more familiar. That doesn't really mean its better.

So yes I’d prefer a Bloch builder solution if a simple constructor is out of the question. But I’d prefer the builder using code looked like this:

TopLevelResponse fooishResponse(
        TopLevelResponseBuilder top,
        MidLevelResponseBuilder mid
) {
    return top
        .newInstance()
        .field1("foo")
        .field2("bar")                                             
        .field3(mid
            .newInstance()
            .field1("baz")
        )
        .build()
    ;
}

Done this way you are not locked into any particular implementation of the builders. I find this way of laying out the code easy on the eyes.

As for rogue implementations, put the builders in the same package as what they build and make the constructor protected and most people won’t be building this on their own.

4
  • I was thinking first case is more of a half-baked builder, with a JavaBeans flavor and a lack of immutability since it just keeps editing a top level object and pulls it whenever.
    – user313675
    Jan 18 '20 at 14:43
  • Your first case (top down) spends a lot of time defending against unneeded nulls. Makes it look like the singleton pattern. If you have a need to do things this way I haven’t seen it yet. Jan 18 '20 at 15:02
  • I have lots of optional fields in the data format, and my approach should be able to handle multiple vendor formats that may or may not have corresponding fields
    – user313675
    Jan 18 '20 at 20:20
  • Figure out what you require to do what you’ve been asked to do and either error out if you don’t get it or quietly do nothing. Jan 18 '20 at 20:27

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy