Статьи

Строитель как конечный автомат (Fail-Fast)

Эта идея пришла ко мне несколько недель назад, когда я проектировал класс «Генератор», который должен был отправлять входные данные инкапсулированному Writer . На самом деле это был образец Строителя. Тем не менее, правила были немного более сложными, пользователь должен был вызывать методы add...() определенным образом для правильной генерации вывода.

Само собой разумеется, мне не нравился вариант иметь один единственный класс BuilderImpl который бы устанавливал и BuilderImpl все виды флагов внутри, чтобы знать, что и когда ему было разрешено делать. Решением было создание конечного автомата , поскольку интерфейс строителя был свободным. Как обычно, в этом посте я проиллюстрирую все это на примере.

Строитель State Machine

Том и Джерри — Проблемы с мышью, Уильям Ханна и Джозеф Барбера

Давайте предположим, что мы хотим реализовать DateBuilder , который генерировал бы String в классическом формате dd.mm.yyyy (возможно, с другими типами разделителей, не только . ). Для простоты мы сосредоточимся только на формате и забудем такие случаи, как количество дней в месяце, високосные годы и т. Д. Сначала идет интерфейс:

01
02
03
04
05
06
07
08
09
10
public interface DateBuilder {
 
    DateBuilder addDay(final Integer day);
    DateBuilder addMonth(final Integer month);
    DateBuilder addYear(final Integer year);
    DateBuilder addSeparator(final String sep);
 
    String build();
 
}

Приведенный выше интерфейс будет иметь пять реализаций: StringDateBuilder (общедоступная точка входа), ExpectSeparator , ExpectMonth , ExpectYear и ExpectBuild (эти четыре являются защищенными пакетами, невидимыми для пользователя). StringDataBuilder выглядит так:

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
public final class StringDateBuilder implements DateBuilder {
 
    private final StringBuilder date = new StringBuilder();
 
    @Override
    public DateBuilder addDay(final Integer day) {
      this.date.append(String.valueOf(day));
      return new ExpectSeparator(this.date);
    }
 
    @Override
    public DateBuilder addMonth(final Integer month) {
      throw new UnsupportedOperationException(
        "A day is expected first! Use #addDay!"
      );
    }
 
    @Override
    public DateBuilder addYear(final Integer year) {
      throw new UnsupportedOperationException(
        "A day is expected first! Use #addDay!"
      );     
    }
 
    @Override
    public DateBuilder addSeparator(final String sep) {
      throw new UnsupportedOperationException(
        "A day is expected first! Use #addDay!"
      );
    }
 
    @Override
    public String build() {
      throw new UnsupportedOperationException(
        "Nothing to build yet! Use #addDay!"
      );
    }
 
}

Я уверен, что вы уже поняли: остальные четыре реализации будут обрабатывать свои собственные ситуации. Например, ExpectSeparator сгенерирует исключение из всех методов, кроме addSeparator(...) , где он добавит разделитель к StringBuilder и вернет экземпляр ExpectMonth . Наконец, последним узлом этой машины будет ExpectBuild (возвращается ExpectYear после добавления года), который будет генерировать исключения из всех методов, кроме build() .

Этот дизайн помог мне сохранить мои объекты кода маленькими, без флагов и разветвлений if/else . Как обычно, каждый из приведенных выше классов легко тестируется, и поведение компоновщика легко изменяется путем переключения возвращаемых реализаций.

Конечно, я не единственный с этими мыслями: г-н. Об этой идее Николя Френкель написал только в прошлом месяце. Однако я почувствовал необходимость принести свои два цента, потому что мне не понравился его пример полностью: он использовал разные интерфейсы для узлов сборщика, пытаясь обеспечить его безопасность и защиту от идиотов (например, даже не позволять пользователю посмотрите метод addMonth или build если они не должны его использовать). Это то, с чем я не согласен, потому что для меня это означает, что мне нужно еще больше кода для управления, и, кроме того, клиент будет больше связан с логикой строителя. Я бы предпочел просто заставить пользователя научиться пользоваться компоновщиком (для него это не должно быть большим усилием, поскольку они должны отлавливать любые исключения с помощью простейших модульных тестов, верно? Верно … )

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

Опубликовано на Java Code Geeks с разрешения MIhai Andronache, партнера нашей программы JCG. Смотрите оригинальную статью здесь: Builder As A (Fail-Fast) State Machine

Мнения, высказанные участниками Java Code Geeks, являются их собственными.