Elegant Builder in Java

    Surely most of any experienced programmers are familiar with the Builder pattern. It allows you to make the initialization of data structures more visual, flexible while maintaining their useful property as immutability. Here is a classic example from the first page of issuing Google to the query "java builder pattern example". With all its advantages, the most important drawback of this pattern implementation is that it has twice as much code as a regular flat bean. If the generation of this additional code is not a problem for any popular IDE, then editing such a class becomes quite tedious and readability suffers anyway.

    At some point, I decided that it was enough to endure it and went in search of an alternative. I must say, the alternative was found quite quickly. Java has a rarely used mechanism for non-static inner classes. An instance of such a class can only be created through an instance of the parent class using the .new operator. What is important, such an object has access to the private fields of its parent.

    So we have an immutable structure

    public class Account {
        private final String userId;
        private final String token;
        public Account(String token, String userId) {
            this.token = token;
            this.userId = userId;
        }
        public String getUserId() {
            return userId;
        }
        public String getToken() {
            return token;
        }
    }
    

    There are only two fields here now, but the builder would still be useful so as not to confuse the order of the parameters in the constructor and if you need to initialize only one field from two or both, but at different points in time. What can I say when the fields will be 20!

    In order not to duplicate the fields in the builder class, we simply start the inner class. He has access to the private fields of his parent class and can expose them directly. We will make the private constructor of the class private, and remove the final modifier from the fields.

    public class Account {
        private String userId;
        private String token;
        private Account() {
            // private constructor
        }
        public String getUserId() {
            return userId;
        }
        public String getToken() {
            return token;
        }
        public class Builder {
            private Builder() {
                // private constructor
            }
            public Builder setUserId(String userId) {
                Account.this.userId = userId;
                return this;
            }
            public Builder setToken(String token) {
                Account.this.token = token;
                return this;
            }
            public Account build() {
                return Account.this;
            }
        }
    }
    

    The builder’s constructor is also private, otherwise, having access to the Account instance, you could make a builder and change the fields of an already created object through it. The build method simply returns a ready-made object (here you can check whether all required fields are in place, for example.

    The final touch is to add to the method to create an instance of the builder.

    public class Account {
        private String userId;
        private String token;
        private Account() {
            // private constructor
        }
        public String getUserId() {
            return userId;
        }
        public String getToken() {
            return token;
        }
        public static Builder newBuilder() {
            return new Account().new Builder();
        }
        public class Builder {
            private Builder() {
                // private constructor
            }
            public Builder setUserId(String userId) {
                Account.this.userId = userId;
                return this;
            }
            public Builder setToken(String token) {
                Account.this.token = token;
                return this;
            }
            public Account build() {
                return Account.this;
            }
        }
    }
    

    Compare with traditional implementation:

    public class Account {
        private final String userId;
        private final String token;
        public Account(String userId, String token) {
            this.userId = userId;
            this.token = token;
        }
        public String getUserId() {
            return userId;
        }
        public String getToken() {
            return token;
        }
        public static class Builder {
            private String userId;
            private String token;
            public Builder setUserId(String userId) {
                this.userId = userId;
                return this;
            }
            public Builder setToken(String token) {
                this.token = token;
                return this;
            }
            public Account build() {
                return new Account(userId, token);
            }
        }
    }
    

    Try adding a new field or changing the type of the token field in either case. With an increase in the number of fields, the difference in code size and readability will become more noticeable. Compare the example from the article to which I referred at the beginning of the topic (I changed it to match the styles of the examples):

    public class Person {
        private final String lastName;
        private final String firstName;
        private final String middleName;
        private final String salutation;
        private final String suffix;
        private final String streetAddress;
        private final String city;
        private final String state;
        private final boolean isFemale;
        private final boolean isEmployed;
        private final boolean isHomeOwner;
        public Person(
                final String newLastName,
                final String newFirstName,
                final String newMiddleName,
                final String newSalutation,
                final String newSuffix,
                final String newStreetAddress,
                final String newCity,
                final String newState,
                final boolean newIsFemale,
                final boolean newIsEmployed,
                final boolean newIsHomeOwner) {
            this.lastName = newLastName;
            this.firstName = newFirstName;
            this.middleName = newMiddleName;
            this.salutation = newSalutation;
            this.suffix = newSuffix;
            this.streetAddress = newStreetAddress;
            this.city = newCity;
            this.state = newState;
            this.isFemale = newIsFemale;
            this.isEmployed = newIsEmployed;
            this.isHomeOwner = newIsHomeOwner;
        }
        public String getLastName() {
            return lastName;
        }
        public String getFirstName() {
            return firstName;
        }
        public String getMiddleName() {
            return middleName;
        }
        public String getSalutation() {
            return salutation;
        }
        public String getSuffix() {
            return suffix;
        }
        public String getStreetAddress() {
            return streetAddress;
        }
        public String getCity() {
            return city;
        }
        public String getState() {
            return state;
        }
        public boolean isFemale() {
            return isFemale;
        }
        public boolean isEmployed() {
            return isEmployed;
        }
        public boolean isHomeOwner() {
            return isHomeOwner;
        }
        public static class Builder {
            private String nestedLastName;
            private String nestedFirstName;
            private String nestedMiddleName;
            private String nestedSalutation;
            private String nestedSuffix;
            private String nestedStreetAddress;
            private String nestedCity;
            private String nestedState;
            private boolean nestedIsFemale;
            private boolean nestedIsEmployed;
            private boolean nestedIsHomeOwner;
            public Builder setNestedLastName(String nestedLastName) {
                this.nestedLastName = nestedLastName;
                return this;
            }
            public Builder setNestedFirstName(String nestedFirstName) {
                this.nestedFirstName = nestedFirstName;
                return this;
            }
            public Builder setNestedMiddleName(String nestedMiddleName) {
                this.nestedMiddleName = nestedMiddleName;
                return this;
            }
            public Builder setNestedSalutation(String nestedSalutation) {
                this.nestedSalutation = nestedSalutation;
                return this;
            }
            public Builder setNestedSuffix(String nestedSuffix) {
                this.nestedSuffix = nestedSuffix;
                return this;
            }
            public Builder setNestedStreetAddress(String nestedStreetAddress) {
                this.nestedStreetAddress = nestedStreetAddress;
                return this;
            }
            public Builder setNestedCity(String nestedCity) {
                this.nestedCity = nestedCity;
                return this;
            }
            public Builder setNestedState(String nestedState) {
                this.nestedState = nestedState;
                return this;
            }
            public Builder setNestedIsFemale(boolean nestedIsFemale) {
                this.nestedIsFemale = nestedIsFemale;
                return this;
            }
            public Builder setNestedIsEmployed(boolean nestedIsEmployed) {
                this.nestedIsEmployed = nestedIsEmployed;
                return this;
            }
            public Builder setNestedIsHomeOwner(boolean nestedIsHomeOwner) {
                this.nestedIsHomeOwner = nestedIsHomeOwner;
                return this;
            }
            public Person build() {
                return new Person(
                        nestedLastName, nestedFirstName, nestedMiddleName,
                        nestedSalutation, nestedSuffix,
                        nestedStreetAddress, nestedCity, nestedState,
                        nestedIsFemale, nestedIsEmployed, nestedIsHomeOwner);
            }
        }
    }
    

    And implementation via inner class:

    public class Person {
        private String lastName;
        private String firstName;
        private String middleName;
        private String salutation;
        private String suffix;
        private String streetAddress;
        private String city;
        private String state;
        private boolean isFemale;
        private boolean isEmployed;
        private boolean isHomeOwner;
        private Person() {
            // private constructor
        }
        public String getLastName() {
            return lastName;
        }
        public String getFirstName() {
            return firstName;
        }
        public String getMiddleName() {
            return middleName;
        }
        public String getSalutation() {
            return salutation;
        }
        public String getSuffix() {
            return suffix;
        }
        public String getStreetAddress() {
            return streetAddress;
        }
        public String getCity() {
            return city;
        }
        public String getState() {
            return state;
        }
        public boolean isFemale() {
            return isFemale;
        }
        public boolean isEmployed() {
            return isEmployed;
        }
        public boolean isHomeOwner() {
            return isHomeOwner;
        }
        public static Builder newBuilder() {
            return new Person().new Builder();
        }
        public class Builder {
            private Builder() {
                // private constructor
            }
            public Builder setLastName(String lastName) {
                Person.this.lastName = lastName;
                return this;
            }
            public Builder setFirstName(String firstName) {
                Person.this.firstName = firstName;
                return this;
            }
            public Builder setMiddleName(String middleName) {
                Person.this.middleName = middleName;
                return this;
            }
            public Builder setSalutation(String salutation) {
                Person.this.salutation = salutation;
                return this;
            }
            public Builder setSuffix(String suffix) {
                Person.this.suffix = suffix;
                return this;
            }
            public Builder setStreetAddress(String streetAddress) {
                Person.this.streetAddress = streetAddress;
                return this;
            }
            public Builder setCity(String city) {
                Person.this.city = city;
                return this;
            }
            public Builder setState(String state) {
                Person.this.state = state;
                return this;
            }
            public Builder setFemale(boolean isFemale) {
                Person.this.isFemale = isFemale;
                return this;
            }
            public Builder setEmployed(boolean isEmployed) {
                Person.this.isEmployed = isEmployed;
                return this;
            }
            public Builder setHomeOwner(boolean isHomeOwner) {
                Person.this.isHomeOwner = isHomeOwner;
                return this;
            }
            public Person build() {
                return Person.this;
            }
        }
    }
    

    You can notice that from the point of view of organizing the code, such a class differs from the usual flat bean with fields and getter-setters only in that the setters are grouped in a separate inner class, only a couple of newBuilder () and build () methods are added, a line with the declaration of the inner class and private designers.

    Important notes:

    1. The build method of the builder returns the same object, and if after calling it continue to set fields through the methods of the builder, the fields of the already created object will change. This can be easily fixed if you create a new instance of the object each time:

    public Account build() {
        Account account = new Account();
        account.userId = Account.this.userId;
        account.token = Account.this.token;
        return account;
    }
    

    This returns part of the duplicate code that we were trying to get rid of. Usually, the link to the builder does not leave the method, so I prefer the option that showed first. If you often pass the builder back and forth and reuse it to regenerate objects, use the option as shown above.

    2. Thanks to the commentators, my doubts are dispelled - the object obtained from such a builder is not thread safe due to the fact that the fields in it are not declared as final. If this point is important in your application, it is better to use a classic builder.

    And finally - the use of the builder.

    Account account = Account.newBuilder()
                        .setToken("hello")
                        .setUserId("habr")
                        .build();
    

    so or

    Account.Builder accountBuilder = Account.newBuilder();
    ...
    accountBuilder.setToken("hello");
    ...
    accountBuilder..setUserId("habr");
    return accountBuilder.build();
    

    Again Account.newBuilder () is prettier to my programmer eyes than new Account.Builder (), although this is already a matter of taste.

    All clean code!

    UPD: As often happens on the Habré, comments turned out to be more useful than the topic itself, it is recommended for familiarization.

    Also popular now: