Поиск лучшего соответствия на объекте с обязательными и необязательными полями - PullRequest
6 голосов
/ 28 марта 2019

Предположим, у меня есть ключ / значение, которое имеет следующую структуру.

@Value.Immutable
public interface Key {
  EnumFoo required_enum_one;
  EnumBar required_enum_two;
  Optional<boolean> optional_boolean_one;
  Optional<EnumBaz> optional_enum_two; 
} 

@Value.Immutable
public interface Value {
  Message message; 
} 

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

{foo1, bar1, true, baz1 } => msg1
{foo1, bar1, true, <any value except baz1> } => msg2
{foo1, bar2, <any value>, <any value> } => msg3
{foo2, bar2, <any value>, <any value> } => msg4
If I do a look up of {foo1, bar2, false, baz1}, it should resolve to msg3

Как лучше всего это реализовать? Я думал о добавлении пользовательского @equals к ключу, где он пропускает необязательное сопоставление, когда его нет в коллекции сопоставлений. Что касается коллекции отображений, я думал о списке кортежей ключ / значение, который упорядочен на основе наличия необязательных полей (аналогично приведенному выше блоку кода), так что первый ключ, который соответствует all обязательные поля и большинство дополнительные поля выбраны и соответствующее значение возвращается?

Есть ли лучший подход? В качестве бонуса я повторяю эту модель для разных типов. Есть ли способ получить многоразовое решение?

Ответы [ 3 ]

2 голосов
/ 28 марта 2019

Я решил эту проблему в прошлом, рассматривая «ключ карты» (a java.util.Map) как отдельную задачу по сравнению с «Типом ключа» (интерфейсом, который вы определили в своем q).

Я сделал это с помощью StringListMapKey, который выглядит примерно так:

final class StringListMapKey {
    private List<String> keyParts;

    public StringListMapKey(List<String> keyParts) {
        this.keyParts = keyParts;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == null || !(obj instanceof StringListMapKey)) {
            return false;
        }

        return this.keyParts.equals(((StringListMapKey) obj).keyParts);
    }

    @Override
    public int hashCode() {
        return keyParts.hashCode();
    }
}

Затем вы можете взять экземпляр типа Key (тот, что в вашем вопросе) и преобразовать его в StringListMapKeyиспользуя что-то вроде этого - где вы можете вставить свои общеизвестные / статические отображения:

final class MapKeyFactory {
    private static final String ANY = "$ANY";

    public StringListMapKey createFrom(Key key) {
        List<String> keyParts = new ArrayList<>();

        keyParts.add(key.getEnumFoo()
                        .toString());
        keyParts.add(key.getEnumBar()
                        .toString());

        keyParts.add(deriveMaybeBooleanOneKeyPart(key.isMaybeBooleanOne()));
        keyParts.add(derviceMaybeEnumBazKeyPart(key.getMaybeEnumBaz()));

        return new StringListMapKey(keyParts);
    }

    private String derviceMaybeEnumBazKeyPart(Optional<EnumBaz> maybeEnumBaz) {
        if (!maybeEnumBaz.isPresent()) {
            return ANY;
        }

        EnumBaz enumBaz = maybeEnumBaz.get();

        switch (enumBaz) {
        case BAZ1:
            return "ONE";
        default:
            return ANY;
        }
    }

    private String deriveMaybeBooleanOneKeyPart(Optional<Boolean> maybeBooleanOne) {
        if (!maybeBooleanOne.isPresent()) {
            return ANY;
        }

        Boolean booleanOne = maybeBooleanOne.get();

        if (booleanOne) {
            return "TRUE";
        }

        return ANY;
    }
}

Единственная проблема с этим заключается в том, что вы теряете информацию о типе в фактическом ключе карты (так как это List<String> под капотом).По этой причине я сделал значение эквивалентным Pair<Key, Message>, на случай, если мне понадобится получить доступ к базовым данным в фактическом ключе.

Обоснование здесь заключается в том, что ваш тип Key действительно (или:вероятно!) следует проверять фактические значения полей на равенство, а не производные значения.

Таким образом, в конечном итоге у вас есть Map, который выглядит следующим образом: Map<StringListMapKey, Pair<Key, Message>>.

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

Если производительность становится фактором (это много хеширования / равенства), вы можете рассмотреть «DelimitiedStringMapKey», который превращает ваш Key экземпляр в простой старой строке типа "foo1: bar1: $ ANY: $ ANY".

1 голос
/ 05 апреля 2019

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

    class Matcher {
        Map<Key, Value> rules;
        Matcher(Map<Key, Value> rules)
        {
            this.rules = rules;
        }
        public Optional<Message> findMessage(Key configuration)
        {
            return rules.keySet().stream()
                    .filter(e -> e.required_enum_one == configuration.required_enum_one)
                    .filter(e -> e.required_enum_two == configuration.required_enum_two)
                    .filter(e -> ! e.optional_boolean_one.isPresent() || e.optional_boolean_one.equals(configuration.optional_boolean_one))
                    .filter(e -> ! e.optional_enum_two.isPresent() || e.optional_enum_two.equals(configuration.optional_enum_two))
                    .findFirst()
                    .map(e -> rules.get(e).message);
         }
    }

    // Test
    Matcher matcher = new Matcher(rules);
    Key configuration1 = new Key(foo1, bar1, Optional.of(true), Optional.of(baz1));
    System.out.println("configuration1: " + matcher.findMessage(configuration1));
    Key configuration2 = new Key(foo1, bar1, Optional.of(true), Optional.of(baz2));
    System.out.println("configuration2: " + matcher.findMessage(configuration2));
    Key configuration3 = new Key(foo1, bar2, Optional.of(false), Optional.of(baz2));
    System.out.println("configuration3: " + matcher.findMessage(configuration3));
    Key configuration4 = new Key(foo2, bar2, Optional.of(true), Optional.of(baz1));
    System.out.println("configuration4: " + matcher.findMessage(configuration4));
    Key configuration5 = new Key(foo2, bar1, Optional.of(true), Optional.of(baz1));
    System.out.println("configuration5: " + matcher.findMessage(configuration5));

Вывод:

configuration1: Optional[Message=msg1]
configuration2: Optional[Message=msg2]
configuration3: Optional[Message=msg3]
configuration4: Optional[Message=msg4]
configuration5: Optional.empty

Вот стандартный код со всеми определениями классов и инициализацией объектов для запуска примера:

class EnumBar {
}

class EnumFoo {
}

class EnumBaz {
}

class Message {
    String text;
    Message(String text) {
        this.text = text;
    }
    public String toString() {
        return "Message=" + text;
    }
}

class Key {
    EnumFoo required_enum_one;
    EnumBar required_enum_two;
    Optional<Boolean> optional_boolean_one;
    Optional<EnumBaz> optional_enum_two;
    Key(EnumFoo required_enum_one, EnumBar required_enum_two,
          Optional<Boolean> optional_boolean_one, Optional<EnumBaz> optional_enum_two) {
       this.required_enum_one = required_enum_one;
       this.required_enum_two = required_enum_two;
       this.optional_boolean_one = optional_boolean_one;
       this.optional_enum_two = optional_enum_two;
    }
}

class Value {
    Message message;
    Value(Message message) {
        this.message = message;
    }
}

EnumBar bar1 = new EnumBar();
EnumBar bar2 = new EnumBar();
EnumFoo foo1 = new EnumFoo();
EnumFoo foo2 = new EnumFoo();
EnumBaz baz1 = new EnumBaz();
EnumBaz baz2 = new EnumBaz();

Message msg1 = new Message("msg1");
Message msg2 = new Message("msg2");
Message msg3 = new Message("msg3");
Message msg4 = new Message("msg4");

Optional<Boolean> anyBooleanValue = Optional.empty();
Optional<EnumBaz> anyBazValue = Optional.empty();

final Map<Key, Value> rules = new HashMap<>();
rules.put(new Key(foo1, bar1, Optional.of(true), Optional.of(baz1)), new Value(msg1));
rules.put(new Key(foo1, bar1, Optional.of(true), anyBazValue), new Value(msg2));
rules.put(new Key(foo1, bar2, anyBooleanValue, anyBazValue), new Value(msg3));
rules.put(new Key(foo2, bar2, anyBooleanValue, anyBazValue), new Value(msg4));

Есть только один недостаток: карта представляет собой неупорядоченный список записей, поэтому, если вы возьмете keySet и проведете через него поток, второе правило может быть оценено перед первым правилом. (Это означает, что в нашем примере configuration1 может выводить сообщение 2). Это просто проблема синхронизации с потоками и проблема реализации с наборами. Вы можете исправить это, используя упорядоченный список вместо карты, но тогда вы не сможете оценить все правила параллельно, чтобы сохранить производительность (это означает использование «.parallelStream ()» вместо «.stream ()»). У меня есть идея получше: просто добавьте поле приоритета к своим правилам (Key-Class). Затем вместо применения ".first ()" используйте ".max (...)" относительно приоритета.

1 голос
/ 30 марта 2019

Я предлагаю вам использовать «Шаблон проектирования цепочки ответственности». Вы можете подумать о создании критерия Key, который будет узлом в цепочке. У каждого узла есть свой способ оценки заданных критериев.

https://sourcemaking.com/design_patterns/chain_of_responsibility

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...