Статьи

Погружение в Образец Строителя

Builder модель описана в Банды четырех «Шаблоны проектирования» Книга:

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


Обычная реализация использования шаблона Builder — это свободный интерфейс со следующим кодом вызывающей стороны:

Person person = new PersonBuilder().withFirstName("John").withLastName("Doe") .withTitle(Title.MR).build();

Этот фрагмент кода может быть включен следующим компоновщиком:

public class PersonBuilder {
 
    private Person person = new Person();
 
    public PersonBuilder withFirstName(String firstName) {
 
        person.setFirstName(firstName);
 
        return this;
    }
 
    // Other methods along the same model
    // ...
 
    public Person build() {
 
        return person;
    }
}

Работа Builder достигнута: Personэкземпляр хорошо инкапсулирован, и только build()метод, наконец, возвращает построенный экземпляр. Это обычно, где большинство статей останавливаются, притворяясь, что охватили предмет. К сожалению, в некоторых случаях может потребоваться более глубокая работа.

Допустим, нам нужно некоторое подтверждение обработки окончательного Personэкземпляра, напримерlastName , атрибут является обязательным. Чтобы обеспечить это, мы могли бы легко проверить, находится ли атрибут nullв build()методе и, соответственно, вызвать исключение.

public Person build() {
 
    if (lastName == null) {
 
        throw new IllegalStateException("Last name cannot be null");
    }
 
    return person;
}

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

Person person1 = new PersonBuilder().withFirstName("John").withLastName("Doe").withTitle(Title.MR).build(); // OK
 
Person person2 = new PersonBuilder().withFirstName("John").withTitle(Title.MR).build(); // Doesn't compile

Мы должны обновить наш компоновщик, чтобы он мог либо вернуть себя, либо неверный компоновщик, у которого отсутствует build()метод, как показано на следующей диаграмме. Обратите внимание, что первый PersonBuilderкласс сохраняется, так как точка входа для вызывающего кода не должна справляться с Valid-/, InvaliPersonBuilderесли не хочет.


Это может привести к следующему коду:

public class PersonBuilder {
 
    private Person person = new Person();
 
    public InvalidPersonBuilder withFirstName(String firstName) {
 
        person.setFirstName(firstName);
 
        return new InvalidPersonBuilder(person);
    }
 
    public ValidPersonBuilder withLastName(String lastName) {
 
        person.setLastName(lastName);
 
        return new ValidPersonBuilder(person);
    }
 
    // Other methods, but NO build() methods
}
 
public class InvalidPersonBuilder {
 
    private Person person;
 
    public InvalidPersonBuilder(Person person) {
 
        this.person = person;
    }
 
    public InvalidPersonBuilder withFirstName(String firstName) {
 
        person.setFirstName(firstName);
 
        return this;
    }
 
    public ValidPersonBuilder withLastName(String lastName) {
 
        person.setLastName(lastName);
 
        return new ValidPersonBuilder(person);
    }
 
    // Other methods, but NO build() methods
}
 
public class ValidPersonBuilder {
 
    private Person person;
 
    public ValidPersonBuilder(Person person) {
 
        this.person = person;
    }
 
    public ValidPersonBuilder withFirstName(String firstName) {
 
        person.setFirstName(firstName);
 
        return this;
    }
 
    // Other methods
 
    // Look, ma! I can build
    public Person build() {
 
        return person;
    }
}

Это огромное улучшение, поскольку теперь разработчики могут знать, что во время компиляции их построенный объект недействителен.

Следующий шаг — представить более сложный вариант использования:

  1. Методы построителя должны вызываться в определенном порядке. Например, дом должен иметь фундамент, каркас и крышу. Строительство каркаса требует наличия фундамента, так как строительство крыши требует каркаса.
  2. Еще сложнее, некоторые шаги зависят от предыдущих шагов (например, плоская крыша возможна только с бетонной рамой)

Упражнение оставлено заинтересованным читателям. Ссылки на предлагаемые реализации приветствуются в комментариях.

У нашего проекта есть один недостаток: достаточно просто вызвать setLastName()метод, чтобы квалифицировать нашего сборщика как допустимого, поэтому передача nullотрицает нашу цель проектирования. Проверка nullзначения во время выполнения не будет достаточной для нашей стратегии во время компиляции. Функции языка Scala могут использовать усовершенствование этой конструкции, называемое шаблоном безопасных типов .

Резюме

  1. В реальном программном обеспечении шаблон компоновщика не так легко реализовать, как быстрые примеры, найденные здесь и там
  2. Меньше значит больше: создать простой в использовании DSL (очень) сложно
  3. Scala облегчает работу разработчиков сложных построителей, чем Java