在具有必填字段和可选字段的对象上进行最佳匹配搜索

时间:2019-03-27 23:28:21

标签: java oop design-patterns

假设我有一个具有以下结构的键/值。

@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; 
} 

假设我具有键/值映射,其中对于必填字段的全部和可选字段的 some 组合,都具有消息值。例如:

{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 个答案:

答案 0 :(得分:2)

过去,我通过将“地图的键”(一个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 :(得分:1)

我建议您使用“责任链设计模式”。您可以考虑将“关键条件”创建为链中的一个节点。每个节点都有其评估给定标准的方式。

https://sourcemaking.com/design_patterns/chain_of_responsibility

答案 2 :(得分:1)

您可以使用流将键(别名配置)与地图中的键(别名规则)进行匹配。这是我执行的命令:

    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()”)。我有一个更好的主意:只需将优先级字段添加到您的规则(键类)中即可。然后在优先级上使用“ .max(...)”,而不是应用“ .first()”。