Статьи

Шаблон строителя на практике

Я не буду вдаваться в подробности схемы, потому что уже есть тонны постов и книг, которые объясняют это очень подробно. Вместо этого я расскажу вам, почему и когда вы должны рассмотреть возможность его использования. Тем не менее, стоит отметить, что этот шаблон немного отличается от представленного в книге « Банды четырех» . В то время как исходный шаблон фокусируется на абстрагировании этапов построения, так что, изменяя используемую реализацию компоновщика, мы можем получить другой результат, шаблон, описанный в этом посте, имеет дело с удалением ненужной сложности, возникающей из нескольких конструкторов, нескольких необязательных параметров и чрезмерного использования сеттера.

Представьте, что у вас есть класс со значительным количеством атрибутов, таких как класс User ниже. Давайте предположим, что вы хотите сделать класс неизменным (что, кстати,

если нет действительно веской причины, по которой вам не следует всегда стремиться делать. Но об этом мы поговорим в другом посте).

1
2
3
4
5
6
7
8
public class User {
    private final String firstName;    //required
    private final String lastName;    //required
    private final int age;    //optional
    private final String phone;    //optional
    private final String address;    //optional
...
}

Теперь представьте, что некоторые атрибуты в вашем классе являются обязательными, а другие — необязательными. Как бы вы пошли о создании объекта этого класса? Все атрибуты объявлены как окончательные, поэтому вы должны установить их все в конструкторе, но вы также хотите дать клиентам этого класса возможность игнорировать необязательные атрибуты.

Первым и допустимым вариантом будет иметь конструктор, который принимает только обязательные атрибуты в качестве параметров, один, который принимает все обязательные атрибуты плюс первый необязательный, другой, который принимает два необязательных атрибута и так далее. На что это похоже? Что-то вроде этого:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
public User(String firstName, String lastName) {
    this(firstName, lastName, 0);
}
 
public User(String firstName, String lastName, int age) {
    this(firstName, lastName, age, '');
}
 
public User(String firstName, String lastName, int age, String phone) {
    this(firstName, lastName, age, phone, '');
}
 
public User(String firstName, String lastName, int age, String phone, String address) {
    this.firstName = firstName;
    this.lastName = lastName;
    this.age = age;
    this.phone = phone;
    this.address = address;
}

Хорошая вещь об этом способе создания объектов класса — то, что это работает. Однако проблема с этим подходом должна быть довольно очевидной. Когда у вас есть только пара атрибутов, это не такая уж большая проблема, но с увеличением этого числа код становится все труднее читать и поддерживать. Что еще более важно, код становится все сложнее для клиентов. Какой конструктор я должен вызывать в качестве клиента? Тот с 2 параметрами? Тот с 3? Какое значение по умолчанию для тех параметров, где я не передаю явное значение? Что если я хочу установить значение для адреса, но не для возраста и телефона? В этом случае мне пришлось бы вызвать конструктор, который принимает все параметры, и передать значения по умолчанию для тех, кто меня не волнует. Кроме того, несколько параметров одного типа могут сбивать с толку. Был ли первый String номер телефона или адрес?

Итак, какой другой выбор у нас есть для этих случаев? Мы всегда можем следовать соглашению JavaBeans, где у нас есть конструктор по умолчанию без аргументов и есть сеттеры и геттеры для каждого атрибута. Что-то типа:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class User {
    private String firstName; // required
    private String lastName; // required
    private int age; // optional
    private String phone; // optional
    private String address;  //optional
 
    public String getFirstName() {
        return firstName;
    }
    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }
    public String getLastName() {
        return lastName;
    }
    public void setLastName(String lastName) {
        this.lastName = lastName;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    public String getPhone() {
        return phone;
    }
    public void setPhone(String phone) {
        this.phone = phone;
    }
    public String getAddress() {
        return address;
    }
    public void setAddress(String address) {
        this.address = address;
    }
}

Этот подход, кажется, легче читать и поддерживать. Как клиент, я могу просто создать пустой объект, а затем установить только те атрибуты, которые мне интересны. Так что с ним не так? С этим решением связаны две основные проблемы. Первая проблема связана с наличием экземпляра этого класса в несовместимом состоянии. Если вы хотите создать объект User со значениями для всех его 5 атрибутов, то у объекта не будет завершенного состояния, пока не будут вызваны все методы setX . Это означает, что некоторая часть клиентского приложения может увидеть этот объект и предположить, что он уже создан, хотя на самом деле это не так. Вторым недостатком этого подхода является то, что теперь класс User является изменяемым. Вы теряете все преимущества неизменных объектов.

К счастью, для этих случаев существует третий вариант — шаблон строителя. Решение будет выглядеть примерно так:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
public class User {
    private final String firstName; // required
    private final String lastName; // required
    private final int age; // optional
    private final String phone; // optional
    private final String address; // optional
 
    private User(UserBuilder builder) {
        this.firstName = builder.firstName;
        this.lastName = builder.lastName;
        this.age = builder.age;
        this.phone = builder.phone;
        this.address = builder.address;
    }
 
    public String getFirstName() {
        return firstName;
    }
 
    public String getLastName() {
        return lastName;
    }
 
    public int getAge() {
        return age;
    }
 
    public String getPhone() {
        return phone;
    }
 
    public String getAddress() {
        return address;
    }
 
    public static class UserBuilder {
        private final String firstName;
        private final String lastName;
        private int age;
        private String phone;
        private String address;
 
        public UserBuilder(String firstName, String lastName) {
            this.firstName = firstName;
            this.lastName = lastName;
        }
 
        public UserBuilder age(int age) {
            this.age = age;
            return this;
        }
 
        public UserBuilder phone(String phone) {
            this.phone = phone;
            return this;
        }
 
        public UserBuilder address(String address) {
            this.address = address;
            return this;
        }
 
        public User build() {
            return new User(this);
        }
 
    }
}

Пара важных моментов, на которые стоит обратить внимание:

  • Конструктор User является закрытым, что означает, что этот класс нельзя напрямую создать из клиентского кода.
  • Класс снова неизменен. Все атрибуты являются окончательными, и они установлены в конструкторе. Кроме того, мы предоставляем только добытчики для них.
  • Конструктор использует идиому Fluent Interface, чтобы сделать клиентский код более читабельным (пример мы увидим чуть позже).
  • Конструктор компоновщика получает только необходимые атрибуты, и эти атрибуты являются единственными, которые определены как «финальные» в компоновщике, чтобы гарантировать, что их значения установлены в конструкторе.

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

Как выглядит код клиента, пытающийся создать новый объект User ? Посмотрим:

1
2
3
4
5
6
7
8
public User getUser() {
    return new
            User.UserBuilder('Jhon', 'Doe')
            .age(30)
            .phone('1234567')
            .address('Fake address 1234')
            .build();
}

Довольно аккуратно, не правда ли? Вы можете создать объект User в одну строку кода, и, что самое важное, его очень легко прочитать. Более того, вы убедитесь, что всякий раз, когда вы получаете объект этого класса, он не будет в неполном состоянии.

Эта модель действительно гибкая. Один конструктор может использоваться для создания нескольких объектов путем изменения атрибутов компоновщика между вызовами метода «build». Разработчик может даже автоматически заполнить некоторое сгенерированное поле между каждым вызовом, например, идентификатор или серийный номер.

Важным моментом является то, что, подобно конструктору, строитель может наложить инварианты на свои параметры. Метод сборки может проверить эти инварианты и вызвать исключение IllegalStateException, если они недопустимы.
Очень важно, чтобы они были проверены после копирования параметров из компоновщика в объект и чтобы они были проверены в полях объекта, а не в полях компоновщика. Причина этого заключается в том, что, поскольку компоновщик не является потокобезопасным, если мы проверяем параметры перед тем, как фактически создать объект, их значения могут быть изменены другим потоком между временем проверки параметров и временем их копирования. Этот период времени известен как «окно уязвимости». В нашем примере с пользователем это может выглядеть следующим образом:

1
2
3
4
5
6
7
public User build() {
    User user = new user(this);
    if (user.getAge() 120) {
        throw new IllegalStateException(“Age out of range”); // thread-safe
    }
    return user;
}

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

1
2
3
4
5
6
7
public User build() {
    if (age 120) {
        throw new IllegalStateException(“Age out of range”); // bad, not thread-safe
    }
    // This is the window of opportunity for a second thread to modify the value of age
    return new User(this);
}

Конечным преимуществом этого шаблона является то, что построитель может быть передан методу, который позволяет этому методу создавать один или несколько объектов для клиента, без необходимости для метода знать какие-либо подробности о том, как создаются объекты. Для этого у вас обычно будет простой интерфейс, такой как:

1
2
3
public interface Builder {
    T build();
}

В предыдущем примере User класс UserBuilder мог реализовывать Builder <User> . Тогда у нас может быть что-то вроде:

1
UserCollection buildUserCollection(Builder<? extends User> userBuilder){...}

Ну, это был довольно длинный первый пост. Подводя итог, можно сказать, что шаблон Builder является отличным выбором для классов с несколькими параметрами (это не точная наука, но я обычно беру 4 атрибута, чтобы быть хорошим индикатором для использования шаблона), особенно если большинство из этих параметров по желанию. Вы получаете клиентский код, который легче читать, писать и поддерживать. Кроме того, ваши классы могут оставаться неизменными, что делает ваш код более безопасным.

ОБНОВЛЕНИЕ : если вы используете Eclipse в качестве вашей IDE, оказывается, что у вас есть довольно много плагинов, чтобы избежать большей части кода котельной пластины, который идет с шаблоном. Три, которые я видел:

Я не пробовал ни одного из них лично, поэтому я не могу принять обоснованное решение о том, какой из них лучше. Я считаю, что подобные плагины должны существовать для других IDE.

Ссылка: Практический пример построения от нашего партнера по JCG Хосе Луиса из отдела развития так, как это должно быть в блоге.