Использование Builder для создания инкапсулированного объекта - PullRequest
3 голосов
/ 03 июля 2019

Представьте, что у меня есть класс, который инкапсулирует другой класс:

@Builder
public class Dragon {

  private Dimensions dimensions;
  private String name;

  public static class ParentBuilder {
    DimensionsBuilder innerBuilder = Dimensions.builder();

    public DragonBuilder height(double height) {
      this.innerBuilder.height(height);
      return this;
    }

    public DragonBuilder length(double length) {
      this.innerBuilder.length(length);
      return this;
    }

    public Dragon build() {
      return Dragon.builder()
        .dimensions(this.innerBuilder.build())
        .name(this.name)
        .build();
    }
  } 
}

@Builder
public class Dimensions {
  private double height;
  private double length;
}

Имейте в виду, что это очень упрощенный пример: реальный код (к сожалению, не о драконах) делегирует много свойств innerBuilder.

Таким образом, я могу создать экземпляр класса следующим образом:

Dragon dragon = Dragon.builder()
  .height(12.0)
  .length(25.0)
  .name("Smaug")
  .build();

Вместо этого:

Dragon dragon = Dragon.builder()
  .dimensions(Dimensions.builder()
    .height(12.0)
    .length(25.0)
    .build())
  .name("Smaug")
  .build;

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

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

1 Ответ

0 голосов
/ 04 июля 2019

На мой взгляд, нет ничего принципиально неправильного в вашем подходе с точки зрения стиля / дизайна.Однако, как объяснил пользователь JB Nizet в комментариях, есть две основные проблемы:

  1. Вы сталкиваетесь с проблемами обслуживания, потому что вам приходится дублировать каждый метод стороннего компоновщика.(Lombok @Delegate не может вам здесь помочь, потому что он не работает с классами, сгенерированными самим Lombok.)
  2. Пользователи вашего компоновщика могут вызывать как dimensions(Dimensions), так и методы делегирования, что очень запутанно.

С точки зрения пользователя, я бы хотел, чтобы конструктор использовался следующим образом:

Dragon dragon = Dragon.builder()
    .dimensions()
        .height(12.0)
        .length(25.0)
        .back()
    .name("Smaug")
    .build();

Вот как вы можете этого достичь (используя Lombok 1.18.8):

@Builder
public class Dragon {
    private Dimensions dimensions;
    private String name;

    public static class DragonBuilder {

        private Dimensions.DimensionsBuilder innerBuilder = 
                new Dimensions.DimensionsBuilder(this);

        // If a method of the same name exists, Lombok does not generate
        // another one even if the parameters differ.
        // In this way, users cannot set their own dimensions object.
        public Dimensions.DimensionsBuilder dimensions() {
            return innerBuilder;
        }

        // Customize build() so that your innerBuilder is used to create 
        // the Dimensions instance.
        public Dragon build() {
            return new Dragon(innerBuilder.build(), name);
        }
    }
}

Строитель для Dimensions содержит ссылку на контейнер DragonBuilder:

// Don't let Lombok create a builder() method, so users cannot 
// instantiate builders on their own.
@Builder(builderMethodName = "")
public class Dimensions {
    private double height;
    private double length;

    public static class DimensionsBuilder {
        private Dragon.DragonBuilder parentBuilder;

        // The only constructor takes a reference to the containing builder.
        DimensionsBuilder(Dragon.DragonBuilder parentBuilder) {
            this.parentBuilder = parentBuilder;
        }

        // Provide a method that returns the containing builder.
        public Dragon.DragonBuilder back() {
            return parentBuilder;
        }

        // The build() method should not be called directly, so 
        // we make it package-private.
        Dimensions build() {
            return new Dimensions(height, length);
        }
    }
}

Этот подход масштабируется, потому что Lombok автоматически генерирует все необходимые оставшиеся методы установки в построителях.Кроме того, не может быть сюрпризов из-за того, что пользователи предоставляют свой собственный экземпляр Dimensions.(Вы могли бы допустить это, но я настоятельно рекомендую делать проверки во время выполнения на наличие потенциальных конфликтов, например, проверяя, были ли вызваны оба метода.)

Недостатком является то, что Dimensions.builder() больше не доступен, поэтомуего нельзя использовать напрямую или в построителях других классов, имеющих поле Dimensions.Однако для этого также есть решение: используйте @SuperBuilder Dimensions и определите class NestedDimensionsBuilder extends Dimensions.DimensionsBuilder<Dimensions, NestedDimensionsBuilder> в пределах DragonBuilder.

...