Установите значение не более одного раза с шаблоном построителя

Существует ли стандартная практика в Java при использовании шаблона компоновщика, чтобы гарантировать, что переменная-член устанавливается не более одного раза. Мне нужно убедиться, что сеттер вызывается 0 или 1 раз, но никогда больше. Я хотел бы бросить RuntimeException некоторого типа, но я обеспокоен вопросами синхронизации, а также передовым опытом в этой области.

4 ответа

Решение

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

Есть несколько способов выполнить то, что вы описываете, давайте рассмотрим их:

  1. Просто дайте пользователям делать то, что они хотят

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

    List<Person> jonesFamily = new ArrayList<>();
    Person.Builder builder = new Person.Builder().setLastName("Jones");
    
    for(String firstName : jonesFamilyFirstNames) {
      family.add(builder.setFirstName(firstName).build());
    }
    

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

  2. Поднять исключение

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

    public class Builder {
      private Object optionalObj = null;
      // ...
    
      public Builder setObject(Object setOnce) {
        checkState(optionalObj == null, "Don't call setObject() more than once");
        optionalObj = setOnce;
      }
      // ...
    }
    

    Это поднимает IllegalStateException так что вы можете просто позвонить throw new IllegalStateException() если вы не используете гуаву (вы должны быть...:)). Предполагая, что вы не передаете объекты компоновщика между потоками, у вас не должно быть проблем с синхронизацией. Если да, то вам следует подумать, зачем вам нужен один и тот же компоновщик в разных потоках - это почти наверняка анти-паттерн.

  3. Не предоставляйте метод вообще

    Это самый чистый и ясный способ запретить пользователю вызывать метод, который вам не нужен - не предоставляйте его в первую очередь. Вместо этого переопределите либо конструктор строителя, либо build() метод, чтобы они могли по желанию передать значение в это время, но не в другое время. Таким образом, вы четко гарантируете, что значение может быть установлено не более одного раза для каждого построенного объекта.

    public class Builder {
      // ...
    
      public Obj build() { ... }
      public Obj build(Object onceOnly) { ... }
    }
    
  4. Используйте разные типы для предоставления определенных методов

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

    public class Builder {
      // contains regular builder methods
    }
    
    public class UnsetBuilder extends Builder {
      public Builder setValue(Object obj) { ... }
    }
    
    // the builder constructor actually returns an UnsetBuilder
    public static UnsetBuilder builder() { ... }
    

    Тогда мы можем назвать что-то вроде:

    builder().setValue("A").build();
    

    Но мы получили бы ошибку времени компиляции, если бы попытались вызвать:

    builder().setValue("A").setValue("B").build();
    

    так как setValue() возвращается Builder которой не хватает setValue() метод, поэтому предотвращение второго случая. Это было бы непросто сделать правильно (что, если пользователь разыгрывает Builder вернуться к UnsetBuilder?) но с некоторым усилием сделаю то что ты ищешь.

Object objectToSet;

boolean isObjectSet = false;

void setObject(Object object) throws RuntimeException {
    if(!isObjectSet) {
        objectToSet=object;
        isObjectSet=true;
    } else {
       throw new RuntimeException();
    }

}

Это стандартная практика.

Вы можете использовать шаблон Builder, изложенный в Effective Java. Это не совсем соответствует вашим требованиям, но я думаю, что это должно соответствовать вашим потребностям. Дай мне знать.

class Foo {
    private final Object objectToSet;

    private Foo(Foo.Builder builder) { 
        objectToSet = builder.getObjectToSet();
    }

    public static class Builder {
        private Object objectToSet; 

        public Builder() { } 

        private Object getObjectToSet() {
            return objectToSet;
        }

        public Builder objectToSet(Object objectToSet) { 
            this.objectToSet = objectToSet; 
            return this; 
        }

        public Foo build() { 
            return new Foo(this);
        }
    }
}

Тогда позже:

Object someObject = 10;
Foo foo = new Foo.Builder()
    .objectToSet(someObject)
    .build(); 
// A Foo's `objectToSet` can only be set once

Это позволяет вам определить класс (в этом случае Foo), который имеет свойства, которые могут быть установлены только один раз. В качестве бонуса вы можете сделать Foo действительно неизменный.

Вы можете определить свою переменную как final,

final MyClass object;

РЕДАКТИРОВАТЬ: Если он устанавливает более одного раза, вы получите ошибку во время компиляции.


Или, как уже упоминалось, вы можете проверить состояние переменной в установщике и для обеспечения безопасности потока объявить метод установщика как synchronized,

public synchronized void setMyObject(Object object)
{
    if (this.myObject == null)
    {
        this.myObject = object;
    } else {
        throw new RuntimeException();
    }
}

Используйте метод setter даже в конструкторе или где-либо еще.

Примечание: использование synchronized Методы могут привести к нестабильности производительности при огромной обработке.

Другие вопросы по тегам