Improving builder pattern by doing validations at

2019-09-05 06:55发布

问题:

I recently started using Builder pattern in one of my projects and I am trying to add some sort of validations on my Builder class. I am assuming we cannot do this at compile time so that's why I am doing this validation at runtime. But may be I am wrong and that's what I am trying to see whether I can do this at compile time.

Traditional builder pattern

public final class RequestKey {

    private final Long userid;
    private final String deviceid;
    private final String flowid;
    private final int clientid;
    private final long timeout;
    private final boolean abcFlag;
    private final boolean defFlag;
    private final Map<String, String> baseMap;

    private RequestKey(Builder builder) {
        this.userid = builder.userid;
        this.deviceid = builder.deviceid;
        this.flowid = builder.flowid;
        this.clientid = builder.clientid;
        this.abcFlag = builder.abcFlag;
        this.defFlag = builder.defFlag;
        this.baseMap = builder.baseMap.build();
        this.timeout = builder.timeout;
    }

    public static class Builder {
        protected final int clientid;
        protected Long userid = null;
        protected String deviceid = null;
        protected String flowid = null;
        protected long timeout = 200L;
        protected boolean abcFlag = false;
        protected boolean defFlag = true;
        protected ImmutableMap.Builder<String, String> baseMap = ImmutableMap.builder();

        public Builder(int clientid) {
            checkArgument(clientid > 0, "clientid must not be negative or zero");
            this.clientid = clientid;
        }

        public Builder setUserId(long userid) {
            checkArgument(userid > 0, "userid must not be negative or zero");
            this.userid = Long.valueOf(userid);
            return this;
        }

        public Builder setDeviceId(String deviceid) {
            checkNotNull(deviceid, "deviceid cannot be null");
            checkArgument(deviceid.length() > 0, "deviceid can't be an empty string");
            this.deviceid = deviceid;
            return this;
        }

        public Builder setFlowId(String flowid) {
            checkNotNull(flowid, "flowid cannot be null");
            checkArgument(flowid.length() > 0, "flowid can't be an empty string");
            this.flowid = flowid;
            return this;
        }

        public Builder baseMap(Map<String, String> baseMap) {
            checkNotNull(baseMap, "baseMap cannot be null");
            this.baseMap.putAll(baseMap);
            return this;
        }

        public Builder abcFlag(boolean abcFlag) {
            this.abcFlag = abcFlag;
            return this;
        }

        public Builder defFlag(boolean defFlag) {
            this.defFlag = defFlag;
            return this;
        }

        public Builder addTimeout(long timeout) {
            checkArgument(timeout > 0, "timeout must not be negative or zero");
            this.timeout = timeout;
            return this;
        }

        public RequestKey build() {
            if (!this.isValid()) {
                throw new IllegalStateException("You have to pass at least one"
                        + " of the following: userid, flowid or deviceid");
            }
            return new RequestKey(this);
        }

        private boolean isValid() {
            return !(TestUtils.isEmpty(userid) && TestUtils.isEmpty(flowid) && TestUtils.isEmpty(deviceid));
        }
    }

    // getters here
}

Problem Statement:

As you can see I have various parameters but only one parameter clientId is mandatory and rest of them are optional. In my above code, I need to have either userid, flowid or deviceid set. If none of those three is set then I am throwing IllegalStateException with an error message as shown above in the code. If all three or two is set then it's fine and I am doing some priority logic on those three or two to decide which one to use but atleast one of them has to be set.

It is not mandatory that they will pass all three id's everytime, they can pass all three or sometimes two or sometimes only one but the condition is either one of them should be set.

What I am looking for is - Instead of doing all these things at runtime, can I do this at compile time and don't build my builder pattern unless either of these three is set and at compile time it should tell what is missing?

I found out this SO link which exactly talks about same thing but not sure how can I use it in my scenario? And also this builder pattern with a twist and this SO question

回答1:

From your description I would go with the solutions you already mentioned: How to improve the builder pattern? and Builder pattern with a twist with a slight variation:

Both these solutions basically chain builder after another: You call Builder.create().firstMandatoryField() which will return an instance of the builder for the second mandatory field and so on until you reach the last builder, which has the actually build method which calls the private constructor.

As in your case there are some fields where at least one of them must be set, that would mean, that your first builder would provide methods to initialize them and return an instance of the second builder. On the second builder you then can set all fields (the optionals as well as the mandatory ones).

This is one version to achieve this:

public final class RequestKey {

    private final Long userid;
    private final String deviceid;
    private final String flowid;
    private final int clientid;
    private final long timeout;
    private final boolean abcFlag;
    private final boolean defFlag;
    private final Map<String, String> baseMap;

    private RequestKey(FinalBuilder builder) {
        this.userid = builder.userid;
        this.deviceid = builder.deviceid;
        this.flowid = builder.flowid;
        this.clientid = builder.clientid;
        this.abcFlag = builder.abcFlag;
        this.defFlag = builder.defFlag;
        this.baseMap = builder.baseMap.build();
        this.timeout = builder.timeout;
    }
    public static class Builder {
        public Builder1 clientId(int clientid) {
            checkArgument(clientid > 0, "clientid must not be negative or zero");
            return new Builder1(clientid);
        }
    }
    public static class Builder1 {
        private final int clientid;

        Builder1(int clientid){
            this.clientid = clientid;
        }
        public FinalBuilder userId(long userid) {
            checkArgument(userid > 0, "userid must not be negative or zero");
            FinalBuilder builder = new FinalBuilder(clientid);
            return builder.setUserId(userid);
        }

        public FinalBuilder deviceId(String deviceid) {
            checkNotNull(deviceid, "deviceid cannot be null");
            checkArgument(deviceid.length() > 0, "deviceid can't be an empty string");
            FinalBuilder builder = new FinalBuilder(clientid);
            return builder.setDeviceId(deviceid);
        }

        public FinalBuilder flowId(String flowid) {
            checkNotNull(flowid, "flowid cannot be null");
            checkArgument(flowid.length() > 0, "flowid can't be an empty string");
            FinalBuilder builder = new FinalBuilder(clientid);
            return builder.setFlowId(flowid);
        }
    }

    public static class FinalBuilder {
        private final int clientid;
        private Long userid = null;
        private String deviceid = null;
        private String flowid = null;
        private long timeout = 200L;
        private boolean abcFlag = false;
        private boolean defFlag = true;
        private ImmutableMap.Builder<String, String> baseMap = ImmutableMap.builder();

        FinalBuilder(int clientId) {
            this.clientid = clientId;
        }


        FinalBuilder setUserId(long userid) {
            this.userid = userid;
            return this;
        }

        FinalBuilder setDeviceId(String deviceid) {
            this.deviceid = deviceid;
            return this;
        }

        FinalBuilder setFlowId(String flowid) {
            this.flowid = flowid;
            return this;
        }
        public FinalBuilder userId(long userid) {
            checkArgument(userid > 0, "userid must not be negative or zero");
            this.userid = Long.valueOf(userid);
            this.userid = userid;
            return this;
        }

        public FinalBuilder deviceId(String deviceid) {
            checkNotNull(deviceid, "deviceid cannot be null");
            checkArgument(deviceid.length() > 0, "deviceid can't be an empty string");
            this.deviceid = deviceid;
            return this;
        }

        public FinalBuilder flowId(String flowid) {
            checkNotNull(flowid, "flowid cannot be null");
            checkArgument(flowid.length() > 0, "flowid can't be an empty string");
            this.flowid = flowid;
            return this;
        }

        public FinalBuilder baseMap(Map<String, String> baseMap) {
            checkNotNull(baseMap, "baseMap cannot be null");
            this.baseMap.putAll(baseMap);
            return this;
        }

        public FinalBuilder abcFlag(boolean abcFlag) {
            this.abcFlag = abcFlag;
            return this;
        }

        public FinalBuilder defFlag(boolean defFlag) {
            this.defFlag = defFlag;
            return this;
        }

        public FinalBuilder addTimeout(long timeout) {
            checkArgument(timeout > 0, "timeout must not be negative or zero");
            this.timeout = timeout;
            return this;
        }

        public RequestKey build() {
            return new RequestKey(this);
        }

    }
    public static Builder create() {
        return new Builder();
    }


    // getters here
}

Then you can call it with:

RequestKey.create()
    .clientId(1234) // Builder of the first level for the mandatory field
    .userId(549375349) // Builder of the second level for any of the additional three mandatory fields
    .flowId("flow number") // Builder on the last level allows setting and overriding the three additional mandatory fields
    .timeout(3600*1000) // Builder on the last level allows setting of the optional fields
    .build(); // Create the instance