Элегантный Builder на Java

в 13:33, , рубрики: builder, java, patterns

Наверняка большинство сколько-нибудь опытных программистов знакомы с паттерном Builder. Он позволяет сделать инициализацию структур данных более наглядной, гибкой при этом сохраняя такое полезное их свойство как неизменяемость (immutability). Вот классический пример с первой страницы выдачи гугла на запрос «java builder pattern example». При всех своих преимуществах, самый главный недостаток данной реализации паттерна — в два раза больше кода, по сравнению с обычным плоским бином. Если генерация этого дополнительного кода не проблема для любой популярной IDE, то редактировать такой класс становится достаточно утомительно и читабельность страдает в любом случае.

В какой-то момент я решил, что хватит это терпеть и занялся поисками альтернативы. Надо сказать, альтернатива нашлась достаточно быстро. В Java есть редко использующийся механизм нестатических внутренних классов. Экземпляр такого класса можно создать только через экземпляр класса-родителя с помощью оператора .new. Что важно, такой объект имеет доступ к приватным полям своего родителя.

Итак, у нас есть неизменяемая структура

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;
    }

}

Здесь сейчас всего два поля, но билдер все равно был бы полезен чтобы не путать порядок параметров в конструкторе и если понадобится проинициализировать только одно поле из двух или оба, но в разные моменты времени. Что говорить, когда полей станет 20!

Чтобы не дублировать поля в классе-билдере, просто заводим внутренний класс. Он имеет доступ к приватным полям своего родительского класса и может выставлять их напрямую. Собственный конструктор класса сделаем приватным, а с полей уберем модификатор final.

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;
        }

    }

}

Конструктор у билдера тоже приватный, иначе имея доступ к экземляру Account можно было бы сделать билдер и через него изменять поля уже созданного объекта. Метод build просто возвращает уже готовый объект (здесь можно проверять все ли обязательные поля на месте, например.

Последний штрих — добавляем в метод для создания экземпляра билдера.

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;
        }

    }

}

Сравните с традиционной реализацией:

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);
        }
        
    }

}

Попробуйте добавить новое поле или изменить тип поля token в том и другом случае. С увеличением количества полей разница в объеме кода и читабельности будет все более заметной. Сравним пример из статьи, на которую я ссылался в начале топика (я его изменил, чтобы стили примеров совпадали):

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);
        }

    }

}

И реализация через внутренний класс:

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;
        }

    }

}

Можно заметить, что с точки зрения организации кода такой класс отличается от обычного плоского бина с полями и гетерами-сетерами только тем, что сеттеры сгруппированы в отдельном внутреннем классе, добавились только пара методов newBuilder() и build(), строчка с объявлением внутреннего класса и приватные конструкторы.

Важное замечание:

Метод build билдера возвращает один и тот же объект и если после его вызова продолжить выставлять поля через методы билдера, поля уже созданного объекта будут меняться. Это легко исправить, если создавать каждый раз новый экземпляр объекта:

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

    return account;
}

При этом возвращается часть дублирующего кода, от которого мы пытались избавиться. Обычно ссылка на билдер не покидает метод, поэтому я предпочитаю вариант, который показал сначала. Если вы часто передаете билдер туда-сюда и переиспользуете его для повторной генерации объектов, используйте вариант, как показано выше.

И напоследок — использование билдера.

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

Ну или

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

return accountBuilder.build();

Опять же Account.newBuilder() моим программерским глазам милее, чем new Account.Builder(), хотя это уже дело вкуса.

Всем чистого кода!

Автор: nhekfqn

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js