Что является хорошим способом разрешить только одно ненулевое поле в объекте

Я хочу написать класс с более чем 1 полем различных типов, но в любое время есть одно и только одно поле объекта экземпляра, имеющее ненулевое значение.

То, что я сделал до сих пор, не выглядит действительно чистым.

class ExclusiveField {

    private BigInteger numericParam;
    private String stringParam;
    private LocalDateTime dateParam;

    public void setNumericParam(BigInteger numericParam) {
        unsetAll();
        this.numericParam = Objects.requireNonNull(numericParam);
    }

    public void setStringParam(String stringParam) {
        unsetAll();
        this.stringParam = Objects.requireNonNull(stringParam);
    }

    public void setDateParam(LocalDateTime dateParam) {
        unsetAll();
        this.dateParam = Objects.requireNonNull(dateParam);
    }

    private void unsetAll() {
        this.numericParam = null;
        this.stringParam = null;
        this.dateParam = null;
    }
}

Java как-то поддерживает этот шаблон или есть более приличный способ сделать это?

Ответы

Ответ 1

Самый простой подход для объекта, который имеет только одно null поле non-, состоит в том, чтобы фактически иметь только одно поле и предполагать, что все остальные неявно равны null. Вам нужно только другое поле тега, чтобы определить, какое поле non- является null.

Поскольку в вашем примере все альтернативы относятся к типу значения, сам тип может быть значением тега, например

class ExclusiveField {
    private Class<?> type;
    private Object value;

    private <T> void set(Class<T> t, T v) {
        value = Objects.requireNonNull(v);
        type = t;
    }
    private <T> T get(Class<T> t) {
        return type == t? t.cast(value): null;
    }

    public void setNumericParam(BigInteger numericParam) {
        set(BigInteger.class, numericParam);
    }

    public BigInteger getNumericParam() {
        return get(BigInteger.class);
    }

    public void setStringParam(String stringParam) {
        set(String.class, stringParam);
    }

    public String getStringParam() {
        return get(String.class);
    }

    public void setDateParam(LocalDateTime dateParam) {
        set(LocalDateTime.class, dateParam);
    }

    public LocalDateTime getDateParam() {
        return get(LocalDateTime.class);
    }
}

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

class ExclusiveField {
    private static final class Key<T> {
        static final Key<String>        STRING_PROPERTY_1 = new Key<>();
        static final Key<String>        STRING_PROPERTY_2 = new Key<>();
        static final Key<BigInteger>    BIGINT_PROPERTY   = new Key<>();
        static final Key<LocalDateTime> DATE_PROPERTY     = new Key<>();
    }
    private Key<?> type;
    private Object value;

    private <T> void set(Key<T> t, T v) {
        value = Objects.requireNonNull(v);
        type = t;
    }

    @SuppressWarnings("unchecked") // works if only set() and get() are used
    private <T> T get(Key<T> t) {
        return type == t? (T)value: null;
    }

    public void setNumericParam(BigInteger numericParam) {
        set(Key.BIGINT_PROPERTY, numericParam);
    }

    public BigInteger getNumericParam() {
        return get(Key.BIGINT_PROPERTY);
    }

    public void setString1Param(String stringParam) {
        set(Key.STRING_PROPERTY_1, stringParam);
    }

    public String getString1Param() {
        return get(Key.STRING_PROPERTY_1);
    }

    public void setString2Param(String stringParam) {
        set(Key.STRING_PROPERTY_2, stringParam);
    }

    public String getString2Param() {
        return get(Key.STRING_PROPERTY_2);
    }

    public void setDateParam(LocalDateTime dateParam) {
        set(Key.DATE_PROPERTY, dateParam);
    }

    public LocalDateTime getDateParam() {
        return get(Key.DATE_PROPERTY);
    }
}

Ответ 2

Измените unsetAll метод setAll:

private void setAll(BigInteger numericParam, String stringParam, LocalDateTime dateParam) {
    this.numericParam = numericParam;
    this.stringParam = stringParam;
    this.dateParam = dateParam;
}

Затем вызовите из ваших открытых сеттеров, как:

public void setNumericParam(BigInteger numericParam) {
    setAll(Objects.requireNonNull(numericParam), null, null);
}

Обратите внимание, что Objects.requireNonNull оценивается перед setAll, поэтому, если вы передадите null numericParam, это не будет выполнено без изменения какого-либо внутреннего состояния.

Ответ 3

Предисловие: Мой ответ носит более теоретический характер, и описанные в нем методы не очень практичны в Java. Они просто не так хорошо поддерживаются, и вы бы "пошли вразрез", условно говоря. Несмотря на это, я думаю, что это хорошая модель, и я подумал, что поделюсь.

Java-классы являются типами продуктов. Когда class C содержит члены типов T1, T2 ,..., Tn, тогда допустимые значения для объектов класса C являются декартовым произведением значений T1, T2 ,..., Tn. Например, если class C содержит bool (который имеет 2 значения) и byte (который имеет 256 значений), то существует 512 возможных значений объектов C:

  • (false, -128)
  • (false, -127)
  • ...
  • (false, 0)...
  • (false, 127)
  • (true, -128)
  • (true, -127)
  • ...
  • (true, 0)...
  • (true, 127)

В вашем примере теоретически возможные значения ExclusiveField равны numberOfValuesOf(BigInteger.class) * numberOfValuesOf(String) * numberOfValuesOf(LocalDateTime) (обратите внимание на умножение, почему оно называется типом продукта), но не совсем то, что вы хотите, Вы ищете способы устранения огромного набора этих комбинаций, чтобы единственными значениями были значения, когда одно поле не является нулевым, а остальные - нулевым. Есть numberOfValuesOf(BigInteger.class) + numberOfValuesOf(String) + numberOfValuesOf(LocalDateTime). Обратите внимание на добавление, это указывает на то, что вы ищете "тип суммы".

Формально говоря, здесь вы ищете теговое объединение (также называемое вариантом, вариантом записи, типом выбора, дискриминационным объединением, непересекающимся объединением или типом суммы). Теговое объединение - это тип, значения которого являются выбором между одним значением членов. В предыдущем примере, если бы C был типом суммы, было бы только 258 возможных значений: -128, -127 ,..., 0, 127, true, false.

Я рекомендую вам проверить союзы на C, чтобы понять, как это работает. Проблема с C состоит в том, что у его профсоюзов не было возможности "запомнить", какой "случай" был активен в любой данный момент, что в основном противоречит цели "типа суммы". Чтобы исправить это, вы должны добавить "тег", представляющий собой перечисление, значение которого говорит вам о состоянии объединения. "Union" хранит полезную нагрузку, а "tag" указывает тип полезной нагрузки, следовательно, "tagged union".

Проблема в том, что в Java нет такой встроенной функции. К счастью, мы можем использовать иерархии классов (или интерфейсы) для реализации этого. По сути, вам нужно бросать свои собственные каждый раз, когда вам это нужно, что является болью, потому что это требует большого количества шаблонной информации, но концептуально просто: * Для n различных случаев вы создаете n разных частных классов, каждый из которых хранит соответствующие элементы. case * Вы объединяете эти частные классы в общий базовый класс (как правило, абстрактный) или интерфейс. * Вы заключаете эти классы в класс пересылки, который предоставляет общедоступный API-интерфейс, скрывая при этом закрытые внутренние компоненты (чтобы никто другой не смог реализовать ваш интерфейс).,

Ваш интерфейс может иметь n методов, каждый из которых похож на getXYZValue(). Эти методы могут быть сделаны как стандартные методы, где реализация по умолчанию возвращает null (для Object значений, но не работает для примитивов, Optional.empty() (для Optional<T> значения), или throw исключение (грубое, но нет лучшего способа для примитивных значений, таких как int). Мне не нравится этот подход, потому что интерфейс довольно неискренен. Соответствующие типы на самом деле не соответствуют интерфейсу, только 1/4 его.

Вместо этого вы можете использовать шаблон, соответствующий Uhhh, шаблону. Вы создаете метод (например, match), который принимает n различных параметров Function, типы которых соответствуют типам случаев различаемого объединения. Чтобы использовать значение различаемого объединения, вы сопоставляете его и предоставляете n лямбда-выражений, каждое из которых действует как регистр в выражении switch. При вызове, динамическая система диспетчеризации вызывает match реализацию, связанную с конкретным storage объектом, который вызывает правильные один из n функций и передает его значение.

Вот пример:

import java.util.Optional;
import java.util.Arrays;
import java.util.List;

import java.util.function.Function;
import java.util.function.Consumer;

import java.time.LocalDateTime;
import java.time.LocalDateTime;
import java.math.BigInteger;

class Untitled {
    public static void main(String[] args) {
        List<ExclusiveField> exclusiveFields = Arrays.asList(
            ExclusiveField.withBigIntegerValue(BigInteger.ONE),
            ExclusiveField.withDateValue(LocalDateTime.now()),
            ExclusiveField.withStringValue("ABC")
        );

        for (ExclusiveField field : exclusiveFields) {
            field.consume(
                i -> System.out.println("Value was a BigInteger: " + i),
                d -> System.out.println("Value was a LocalDateTime: " + d),
                s -> System.out.println("Value was a String: " + s)
            );
        }
    }
}

class ExclusiveField {
    private ExclusiveFieldStorage storage;

    private ExclusiveField(ExclusiveFieldStorage storage) { this.storage = storage; }

    public static ExclusiveField withBigIntegerValue(BigInteger i) { return new ExclusiveField(new BigIntegerStorage(i)); }
    public static ExclusiveField withDateValue(LocalDateTime d) { return new ExclusiveField(new DateStorage(d)); }
    public static ExclusiveField withStringValue(String s) { return new ExclusiveField(new StringStorage(s)); }

    private <T> Function<T, Void> consumerToVoidReturningFunction(Consumer<T> consumer) {
        return arg -> { 
            consumer.accept(arg);
            return null;
        };
    }

    // This just consumes the value, without returning any results (such as for printing)
    public void consume(
        Consumer<BigInteger> bigIntegerMatcher,
        Consumer<LocalDateTime> dateMatcher,
        Consumer<String> stringMatcher
    ) {
        this.storage.match(
            consumerToVoidReturningFunction(bigIntegerMatcher),
            consumerToVoidReturningFunction(dateMatcher),
            consumerToVoidReturningFunction(stringMatcher)
        );
    }   

    // Transform 'this' according to one of the lambdas, resuling in an 'R'.
    public <R> R map(
        Function<BigInteger, R> bigIntegerMatcher,
        Function<LocalDateTime, R> dateMatcher,
        Function<String, R> stringMatcher
    ) {
        return this.storage.match(bigIntegerMatcher, dateMatcher, stringMatcher);
    }   

    private interface ExclusiveFieldStorage {
        public <R> R match(
            Function<BigInteger, R> bigIntegerMatcher,
            Function<LocalDateTime, R> dateMatcher,
            Function<String, R> stringMatcher
        );
    }

    private static class BigIntegerStorage implements ExclusiveFieldStorage {
        private BigInteger bigIntegerValue;

        BigIntegerStorage(BigInteger bigIntegerValue) { this.bigIntegerValue = bigIntegerValue; }

        public <R> R match(
            Function<BigInteger, R> bigIntegerMatcher,
            Function<LocalDateTime, R> dateMatcher,
            Function<String, R> stringMatcher
        ) {
            return bigIntegerMatcher.apply(this.bigIntegerValue);
        }
    }

    private static class DateStorage implements ExclusiveFieldStorage {
        private LocalDateTime dateValue;

        DateStorage(LocalDateTime dateValue) { this.dateValue = dateValue; }

        public <R> R match(
            Function<BigInteger, R> bigIntegerMatcher,
            Function<LocalDateTime, R> dateMatcher,
            Function<String, R> stringMatcher
        ) {
            return dateMatcher.apply(this.dateValue);
        }
    }

    private static class StringStorage implements ExclusiveFieldStorage {
        private String stringValue;

        StringStorage(String stringValue) { this.stringValue = stringValue; }

        public <R> R match(
            Function<BigInteger, R> bigIntegerMatcher,
            Function<LocalDateTime, R> dateMatcher,
            Function<String, R> stringMatcher
        ) {
            return stringMatcher.apply(this.stringValue);
        }
    }
}

Ответ 4

Почему не просто?

public void setNumericParam(BigInteger numericParam) { 
     this.numericParam = Objects.requireNonNull(numericParam); 
     this.stringParam = null; 
     this.dateParam = null; 
}

Ответ 5

Твоя цель

В комментариях вы упоминаете, что ваша цель - написать SQL-запросы для устаревшей БД:

тип: VARCHAR, числовой: INT, строка: VARCHAR, дата: DATETIME и ExclusiveField будут использоваться как getQueryRunner(). query ("CALL sp_insert_parametter (?,?,?,?,?)", param.getNumericParam(), id, тип, param.getStringParam(), param.getDateParam())

Таким образом, ваша цель на самом деле не состоит в том, чтобы создать класс только с одним ненулевым полем.

альтернатива

Вы можете определить абстрактный класс Field с атрибутами id, type, value:

public abstract class Field
{
    private int id;
    private Class<?> type;
    private Object value;

    public Field(int id, Object value) {
        this.id = id;
        this.type = value.getClass();
        this.value = value;
    }

    public abstract int getPosition();
}

Для каждого столбца в вашей базе данных вы создаете небольшой соответствующий класс, расширяющий Field. Каждый класс определяет свой желаемый тип и свою позицию в команде SQL:

import java.math.BigInteger;


public class BigIntegerField extends Field
{
    public BigIntegerField(int id, BigInteger numericParam) {
        super(id, numericParam);
    }

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

Вы можете определить Field#toSQL:

public String toSQL(int columnsCount) {
    List<String> rows = new ArrayList<>(Collections.nCopies(columnsCount, "NULL"));
    rows.set(getPosition(), String.valueOf(value));
    return String.format("SOME SQL COMMAND (%d, %s, %s)", id, type.getName(), String.join(", ", rows));
}

Который будет выводить NULLS везде, кроме как в нужной позиции.

Это.

Полный код

Field.java

package com.stackoverflow.legacy_field;

import java.math.BigInteger;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;


public abstract class Field
{
    private int id;
    private Class<?> type;
    private Object value;

    public Field(int id, Object value) {
        this.id = id;
        this.type = value.getClass();
        this.value = value;
    }

    public abstract int getPosition();

    public static void main(String[] args) {
        List<Field> fields = Arrays.asList(new BigIntegerField(3, BigInteger.TEN),
                new StringField(17, "FooBar"),
                new DateTimeField(21, LocalDateTime.now()));
        for (Field field : fields) {
            System.out.println(field.toSQL(3));
        }
    }

    public String toSQL(int columnsCount) {
        List<String> rows = new ArrayList<>(Collections.nCopies(columnsCount, "NULL"));
        rows.set(getPosition(), String.valueOf(value));
        return String.format("SOME SQL COMMAND (%d, %s, %s)", id, type.getName(), String.join(", ", rows));
    }
}

BigIntegerField.java

package com.stackoverflow.legacy_field;

import java.math.BigInteger;


public class BigIntegerField extends Field
{
    public BigIntegerField(int id, BigInteger numericParam) {
        super(id, numericParam);
    }

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

StringField.java

package com.stackoverflow.legacy_field;

public class StringField extends Field
{
    public StringField(int id, String stringParam) {
        super(id, stringParam);
    }

    @Override
    public int getPosition() {
        return 1;
    }
}

DateTimeField.java

package com.stackoverflow.legacy_field;

import java.time.LocalDateTime;

public class DateTimeField extends Field
{

    public DateTimeField(int id, LocalDateTime value) {
        super(id, value);
    }

    @Override
    public int getPosition() {
        return 2;
    }
}

Результат

Запуск Field#main выходов:

SOME SQL COMMAND (3, java.math.BigInteger, 10, NULL, NULL)
SOME SQL COMMAND (17, java.lang.String, NULL, FooBar, NULL)
SOME SQL COMMAND (21, java.time.LocalDateTime, NULL, NULL, 2019-05-09T09:39:56.062)

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

Ответ 6

Вы могли бы использовать отражение. Две функции и все готово. Добавить новое поле? Нет проблем. Вам даже не нужно ничего менять.

public void SetExclusiveValue(String param, Object val){
    this.UnsetAll();
    Class cls = this.getClass();
    Field fld = cls.getDeclaredField(param); //Maybe need to set accessibility temporarily? Or some other kind of check.
    //Also need to add check for fld existence!
    fld.set(this, Objects.requireNonNull(val));
}

private void UnsetAll(){
    Class cls = this.getClass();
    Field[] flds = cls.getDeclaredFields();
    for (Field fld : flds){
        fld.set(this,null);
    }
}

Если доступность является проблемой, вы можете просто добавить список доступных полей и проверить param против этого