NullObjectPattern и сопоставимый интерфейс

Проблема, с которой я столкнулся, уже задавалась ранее: как реализовать интерфейс с перечислением, где этот интерфейс расширяет Comparable?

Однако ни одно из решений не решает мою точную проблему, а именно:

У меня есть объект значения, похожий на BigDecimal, Иногда это значение не может быть установлено с реальным объектом, потому что это значение еще не известно. Поэтому я хочу использовать шаблон нулевого объекта для представления времени, когда этот объект не определен. Это все не проблема, пока я не попытаюсь заставить свой Null Object реализовать Comparable интерфейс. Вот SSCCE для иллюстрации:

public class ComparableEnumHarness {
  public static interface Foo extends Comparable<Foo> {
    int getValue();
  }

  public static class VerySimpleFoo implements Foo {
    private final int value;

    public VerySimpleFoo(int value) {
      this.value = value;
    }

    @Override
    public int compareTo(Foo f) {
      return Integer.valueOf(value).compareTo(f.getValue());
    }

    @Override
    public int getValue() {
      return value;
    }
  }

  // Error is in the following line:
  // The interface Comparable cannot be implemented more than once with different arguments:
  // Comparable<ComparableEnumHarness.NullFoo> and Comparable<ComparableEnumHarness.Foo>
  public static enum NullFoo implements Foo {
    INSTANCE;

    @Override
    public int compareTo(Foo f) {
      return f == this ? 0 : -1; // NullFoo is less than everything except itself
    }

    @Override
    public int getValue() {
      return Integer.MIN_VALUE;
    }
  }
}

Другие проблемы:

  • В реальном примере есть несколько подклассов того, что я называю Foo Вот.
  • Я мог бы, вероятно, обойти это, имея NullFoo не быть enum, но тогда я не могу гарантировать, что когда-либо будет только один его экземпляр, т.е. Effective Java Item 3, pg. 17-18

3 ответа

Я не рекомендую шаблон NullObject, потому что я всегда нахожусь в одной из следующих двух ситуаций:

  • не имеет смысла использовать NullObject как объект, и он должен оставаться null
  • NullObject имеет слишком много значения, чтобы быть просто NullObject, и должен быть самим истинным объектом (например, когда он действует как полностью функциональное значение по умолчанию)

Согласно нашему обсуждению в комментариях, мне кажется, что ваш NullObject ведет себя очень похоже на значение 0 ваших обычных объектов.

На самом деле я бы использовал 0 (или любое другое значение по умолчанию, которое имеет больше смысла) и поставил флаг, если вам действительно нужно знать, было ли оно инициализировано. Таким образом, вы будете иметь 2 вещи для рассмотрения:

  • все неинициализированные значения не будут использовать один и тот же экземпляр в моем решении
  • по той же причине вы теперь можете инициализировать свой объект позже без необходимости создавать новый экземпляр

Вот код, о котором я думаю:

public static class VerySimpleFoo implements Foo {
    private int value;
    private boolean initialized;

    public VerySimpleFoo() {
      this.value = 0; // whatever default value makes more sense
      this.initialized = false;
    }

    public VerySimpleFoo(int value) {
      this.value = value;
      this.initialized = true;
    }

    @Override
    public int compareTo(Foo f) {
      // possibly need some distinction here, depending on your default value
      // and the behavior you expect
      return Integer.valueOf(value).compareTo(f.getValue());
    }

    @Override
    public int getValue() {
      return value;
    }

    public void setValue(int value) {
      this.value = value;
      this.initialized = true;
    }

    public boolean isInitialized() {
      return initialized;
    }
}

Как вы и предполагали, я полагаю, что одним из решений будет использование класса вместо перечисления:

public class NullFoo implements Foo {

    private NullFoo() {
    }

    public static final Foo INSTANCE = new NullFoo();

    @Override
    public int compareTo(Foo f) {
        return f == this ? 0 : -1;
    }

    @Override
    public int getValue() {
        return 0;
    }

}

Это имитирует поведение enum, но позволяет реализовать интерфейс Foo. Класс не может быть создан из-за закрытого конструктора, поэтому единственный доступный экземпляр - это тот, который доступен через NullFoo.INSTANCE, который является потокобезопасным (благодаря final модификатор).

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

Я бы отбросил enum для NullFoo, преобразовал его в класс (как вы предложили) и сделал бы окончательную публичную статическую ссылку INSTANCE с приватным конструктором (это не так хорошо, как использование enum, но в большинстве случаев приемлемо).

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