Существует определенный паттерн, который я видел, использованный для такого рода вещей, который является вариантом паттерна Bloch's Typesafe Heterogenous Container.Я не знаю, есть ли у него имя само по себе или нет, но из-за отсутствия лучшего имени я назову его Typesafe Enumerated Lookup Keys.
В принципе, проблема, с которой я столкнулся, возникает вВ различных контекстах вам нужен динамический набор пар ключ / значение, где определенное подмножество ключей «хорошо известно» с предопределенной семантикой.Кроме того, каждый ключ связан с определенным типом.
«Очевидным» решением является использование перечисления.Например, вы могли бы сделать:
public enum LookupKey { FOO, BAR }
public final class Repository {
private final Map<LookupKey, Object> data = new HashMap<>();
public void put(LookupKey key, Object value) {
data.put(key, value);
}
public Object get(LookupKey key) {
return data.get(key);
}
}
Это работает просто отлично, но очевидным недостатком является то, что теперь вам нужно разыгрывать повсюду.Например, предположим, что вы знаете, что LookupKey.FOO
всегда имеет значение String
, а LookupKey.BAR
всегда имеет значение Integer
.Как вы применяете это?В этой реализации вы не можете.
Также: в этой реализации набор ключей фиксируется перечислением.Вы не можете добавлять новые во время выполнения.Для некоторых приложений это преимущество, но в других случаях вы действительно хотите разрешить новые ключи в некоторых случаях.
Решение обеих этих проблем в основном одно и то же: сделать LookupKey
первоклассным объектом, а не просто перечисление.Например:
/**
* A key that knows its own name and type.
*/
public final class LookupKey<T> {
// These are the "enumerated" keys:
public static final LookupKey<String> FOO = new LookupKey<>("FOO", String.class);
public static final LookupKey<Integer> BAR = new LookupKey<>("BAR", Integer.class);
private final String name;
private final Class<T> type;
public LookupKey(String name, Class<T> type) {
this.name = name;
this.type = type;
}
/**
* Returns the name of this key.
*/
public String name() {
return name;
}
@Override
public String toString() {
return name;
}
/**
* Cast an arbitrary object to the type of this key.
*
* @param object an arbitrary object, retrieved from a Map for example.
* @throws ClassCastException if the argument is the wrong type.
*/
public T cast(Object object) {
return type.cast(object);
}
// not shown: equals() and hashCode() implementations
}
Это дает нам большую часть пути уже там.Вы можете ссылаться на LookupKey.FOO
и LookupKey.BAR
, и они ведут себя так, как вы ожидаете, но они также знают соответствующий тип поиска.И вы также можете определить свои собственные ключи, создав новые экземпляры LookupKey
.
. Если мы хотим реализовать некоторые приятные перечислимые возможности, такие как статический метод values()
, нам просто нужно добавить реестр.В качестве бонуса нам даже не нужны equals()
и hashCode()
, если мы добавляем реестр, поскольку теперь мы можем просто сравнивать ключи поиска по идентификатору.
Вот как в итоге выглядит класс:
/**
* A key that knows its own name and type.
*/
public final class LookupKey<T> {
// This is the registry of all known keys.
// (It needs to be declared first because the create() function needs it.)
private static final Map<String, LookupKey<?>> knownKeys = new HashMap<>();
// These are the "enumerated" keys:
public static final LookupKey<String> FOO = create("FOO", String.class);
public static final LookupKey<Integer> BAR = create("BAR", Integer.class);
/**
* Create and register a new key. If a key with the same name and type
* already exists, it is returned instead (Flywheel Pattern).
*
* @param name A name to uniquely identify this key.
* @param type The type of data associated with this key.
* @throws IllegalStateException if a key with the same name but a different
* type was already registered.
*/
public static <T> LookupKey<T> create(String name, Class<T> type) {
synchronized (knownKeys) {
LookupKey<?> existing = knownKeys.get(name);
if (existing != null) {
if (existing.type != type) {
throw new IllegalStateException(
"Incompatible definition of " + name);
}
@SuppressWarnings("unchecked") // our invariant ensures this is safe
LookupKey<T> uncheckedCast = (LookupKey<T>) existing;
return uncheckedCast;
}
LookupKey<T> key = new LookupKey<>(name, type);
knownKeys.put(name, key);
return key;
}
}
/**
* Returns a list of all the currently known lookup keys.
*/
public static List<LookupKey<?>> values() {
synchronized (knownKeys) {
return Collections.unmodifiableList(
new ArrayList<>(knownKeys.values()));
}
}
private final String name;
private final Class<T> type;
// Private constructor. Only the create method should call this.
private LookupKey(String name, Class<T> type) {
this.name = name;
this.type = type;
}
/**
* Returns the name of this key.
*/
public String name() {
return name;
}
@Override
public String toString() {
return name;
}
/**
* Cast an arbitrary object to the type of this key.
*
* @param object an arbitrary object, retrieved from a Map for example.
* @throws ClassCastException if the argument is the wrong type.
*/
public T cast(Object object) {
return type.cast(object);
}
}
Теперь LookupKey.values()
работает более или менее так же, как перечисление.Вы также можете добавить свои собственные ключи, и values()
вернет их позже:
LookupKey<Double> myKey = LookupKey.create("CUSTOM_DATA", Double.class);
Как только у вас появится этот класс LookupKey
, вы теперь можете реализовать типизированное хранилище, которое использует эти ключи для поиска:
/**
* A repository of data that can be looked up using a {@link LookupKey}.
*/
public final class Repository {
private final Map<LookupKey<?>, Object> data = new HashMap<>();
/**
* Set a value in the repository.
*
* @param <T> The type of data that is being stored.
* @param key The key that identifies the value.
* @param value The corresponding value.
*/
public <T> void put(LookupKey<T> key, T value) {
data.put(key, value);
}
/**
* Gets a value from this repository.
*
* @param <T> The type of the value identified by the key.
* @param key The key that identifies the desired value.
*/
public <T> T get(LookupKey<T> key) {
return key.cast(data.get(key));
}
}