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; } }
Это огромное улучшение, поскольку теперь разработчики могут знать, что во время компиляции их построенный объект недействителен.
Следующий шаг — представить более сложный вариант использования:
- Методы построителя должны вызываться в определенном порядке. Например, дом должен иметь фундамент, каркас и крышу. Строительство каркаса требует наличия фундамента, так как строительство крыши требует каркаса.
- Еще сложнее, некоторые шаги зависят от предыдущих шагов (например, плоская крыша возможна только с бетонной рамой)
Упражнение оставлено заинтересованным читателям. Ссылки на предлагаемые реализации приветствуются в комментариях.
У нашего проекта есть один недостаток: достаточно просто вызвать
setLastName()
метод, чтобы квалифицировать нашего сборщика как допустимого, поэтому передачаnull
отрицает нашу цель проектирования. Проверкаnull
значения во время выполнения не будет достаточной для нашей стратегии во время компиляции. Функции языка Scala могут использовать усовершенствование этой конструкции, называемое шаблоном безопасных типов .
Резюме
- В реальном программном обеспечении шаблон компоновщика не так легко реализовать, как быстрые примеры, найденные здесь и там
- Меньше значит больше: создать простой в использовании DSL (очень) сложно
- Scala облегчает работу разработчиков сложных построителей, чем Java