Первоначально я разместил это в Stack overflow, и мне сказали разместить его здесь, так что вот оно. Вот вопрос о переполнении стека: https://stackoverflow.com/questions/66528393/generic-class-for-a-gson-linkedhashmap
Я работал над этим решением несколько месяцев и пришел к выводу, что нет чистого способа достичь того, чего я пытаюсь достичь. Мне кажется, что мое образование в области полиморфизма меня не подводит, поэтому я пришел в StackOverflow, чтобы узнать второе мнение. Извините, если это кажется длинным и запутанным. Это было моим умом последние пару месяцев, и на данный момент у меня нет идей. Я надеюсь, что кто-нибудь сможет взглянуть и увидеть, что я мог бы избежать всего этого беспорядка, сделав это каким-то другим способом.
Я пытаюсь достичь двух общих классов: один, который может представлять любой «сохраняемый» объект, и второй, который может представлять список сохраняемых объектов (или то, что я называю «хранилищем»). Сохраняемый объект может сохранять себя с помощью GSON, а хранилище также может сохранять себя с помощью GSON в файл JSON. Разница в том, что сохраняемые объекты обычно представляют любой объект GSON, который может быть сохранен, тогда как хранилища расширяются от сохраняемых объектов, чтобы стать сохраняемой хэш-картой объектов через идентификаторы.
Пример вывода, который я ищу, таков:
Представьте, что у меня есть объект со строковым полем uuid и полем строки имени. Я хочу иметь возможность создать хранилище этих объектов, которое является LinkedHashMap, но также расширить Saveable, чтобы объекты можно было сохранять следующим образом:
test.json
{"dbf39199209e466ebed0061a3491ed9e":{"uuid":"dbf39199209e466ebed0061a3491ed9e","name":"Example Name"}}
Я также хотел бы иметь возможность загружать этот JSON обратно в объекты с помощью метода загрузки Store.
Пример использования кода будет таким:
Store<User> users = new Store<>();
users.load();
users.add(new User("dbf39199209e466ebed0061a3491ed9e", "Example Name"));
users.save();
Мои попытки
Сохраняемые
Я ожидаю, что «сохраняемый» объект сможет сделать следующее: предоставить не аргументированный метод для сохранения и предоставить не аргументированный метод для загрузки. Сохраняемый объект представляет собой любой объект, который может быть сохранен через GSON. Он содержит два поля: объект Gson gson и местоположение пути. Я добавляю их в конструктор файла save. Затем я хочу предоставить два метода: Saveable#save()
метод и Saveable#load()
метод (или статический Saveable#load()
метод, мне безразлично). Вы используете объект Saveable, расширяя его (чтобы он был абстрактным) до другого объекта, представляющего что-то, скажем, TestSaveable
, а затем использование таково:
TestSaveable saveable = new TestSaveable(8);
saveable.save(); // Saves data
saveable.setData(4);
saveable = saveable.load(); // Loads old data
Я также хотел бы, чтобы сохраняемый объект мог обрабатывать универсальный объект, такой как целое число (подумайте о последнем примере, но с целочисленным универсальным). Это позволило бы мне выполнить мой следующий план в отношении магазинов.
Моя попытка реализации была следующей:
public abstract class Saveable {
private transient Gson gson;
private transient Path location;
public Saveable(Gson gson, Path location) {
this.gson = gson;
this.location = location;
}
@SuppressWarnings("unchecked")
public <T extends Saveable> T save() throws IOException {
if (location.getParent() != null) {
Files.createDirectories(location.getParent());
}
Files.write(location, gson.toJson(this).getBytes(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, LinkOption.NOFOLLOW_LINKS);
return (T) this;
}
protected <T extends Saveable> T load(Class<T> clazz, @NotNull Class<?>... generics) throws IOException {
if (!Files.exists(location)) {
return this.save();
} else {
InstanceCreator<Saveable> creator = type -> this;
Type type = TypeToken.getParameterized(clazz, generics).getType();
Gson newGson = gson.newBuilder().registerTypeAdapter(type, creator).create();
return newGson.fromJson(Files.newBufferedReader(location), type);
}
}
}
К сожалению, эта попытка не достигла моей цели, потому что после создания моего класса TestSaveable пользователям все равно пришлось передать общий для загрузки:
public class TestSaveable<T> extends Saveable {
public boolean testBool = false;
public T value;
public TestSaveable(T value) {
super(new Gson(), Path.of("test.json"));
this.value = value;
}
public final TestSaveable<T> load(Class<T> generic) throws IOException {
return super.load(TestSaveable.class, generic);
}
}
Тем не менее, благодаря этому я получил довольно чистую реализацию, за исключением практически полной проверки типов и необходимости постоянно добавлять для нее подавления:
public class Test {
public static void main(String[] args) {
try {
TestSaveable<Integer> storeB4 = new TestSaveable<>(5).save();
storeB4.value = 10;
TestSaveable<Integer> store = storeB4.load(Integer.class);
System.out.println("STORE: " + store);
} catch (Exception e) {
e.printStackTrace();
}
}
}
магазины
Магазины — это продолжение сохраняемых предметов. Магазин — это LinkedHashMap, который быстро и легко сохранит все объекты в нем в виде карты в GSON. К сожалению, я даже не знаю, с чего начать. Я не могу расширить два объекта (два из них — LinkedHashMap
Ранее я пробовал следующее, используя классы IStorable и ISaveable в качестве альтернативы абстрактному классу Saveable, который я показал вам выше, но это привело к другому очень уродливому и ненадежному решению моей проблемы.
Saveable.java
public class Saveable {
// Suppress default constructor
private Saveable() {}
// Save a class to the specified location using the specified gson
public static <T extends ISaveable> T save(T instance) throws IOException {
Files.createDirectories(instance.getLocation().getParent());
Files.write(instance.getLocation(), instance.getGson().toJson(instance).getBytes(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, LinkOption.NOFOLLOW_LINKS);
return instance;
}
// Load a file from the specified location using the specified gson and cast it to the specified class using the specified generic
public static <T extends ISaveable> ISaveable load(Path location, Gson gson, Class<T> clazz, Class<?> genericClazz) throws IOException {
if (!Files.exists(location)) {
return null;
} else {
TypeToken<?> type = genericClazz == null ? TypeToken.get(clazz) : TypeToken.getParameterized(clazz, genericClazz);
ISaveable saveable = gson.fromJson(Files.newBufferedReader(location), type.getType());
saveable.setGson(gson);
saveable.setLocation(location);
return saveable;
}
}
}
ISaveable.java
public interface ISaveable {
// Gson
Gson getGson();
void setGson(Gson gson);
// Location
Path getLocation();
void setLocation(Path location);
}
IStorable.java
public interface IStoreable {
String getUuid();
}
Store.java
public class Store<T extends IStoreable> extends LinkedHashMap<String, T> implements ISaveable {
private transient Path location;
private transient Gson gson;
public Store(Path location, Gson gson) {
this.location = location;
this.gson = gson;
}
public Store() {
this.location = null;
this.gson = null;
}
public Store<T> put(T value) {
this.put(value.getUuid(), value);
return this;
}
public Store<T> remove(T value) {
this.remove(value.getUuid());
return this;
}
public Store<T> save() throws IOException {
return Saveable.save(this);
}
@SuppressWarnings("unchecked")
public static <T extends IStoreable> Store<T> load(Path location, Gson gson, Class<T> genericClazz) throws IOException {
ISaveable saveable = Saveable.load(location, gson, Store.class, genericClazz);
if (saveable == null) {
return new Store<T>(location, gson).save();
} else {
return (Store<T>) saveable;
}
}
}
Это решение дало мне почти тот результат, который я искал, но быстро не справился с процессом загрузки, а также просто не был надежным решением, за исключением сотен практик Java, которые я уверен, что на этом этапе я испортил:
Store<ExampleStoreable> store = Store.load(Paths.get("storetest.json"), new Gson(), ExampleStoreable.class);
store.put(new ExampleStoreable("Example Name"));
store.save();
И прежде чем я получу какие-либо комментарии о том, что мне не следует публиковать это в StackOverflow: если не здесь, то где еще? Пожалуйста, помогите указать мне правильное направление, я бы хотел, чтобы меня не оставили в темноте.
Спасибо, если кто-то может помочь, и если я не понимаю. В любом случае это не самый простой вопрос.
1 ответ
Почему вам нужно предоставлять методы сохранения и загрузки без аргументов? Это означает, что вы упаковываете логику хранения в объект данных, и это нарушает принцип единой ответственности.
Лично я даже не хочу видеть аннотации, относящиеся к определенному механизму хранения в объектах данных (и я знаю, что это уровень фундаментализма, которого не разделяют многие люди). Почему объект данных должен знать о механизме хранения? Данные не должны знать, как и где они хранятся. Это ответственность уровня сохраняемости. И как только слой сохраняемости изменится на новейший модный формат сериализации, все объекты данных должны быть изменены, даже если их основное назначение и ответственность немного не изменились.
Потому что твой ISaveable
интерфейс требует Path
Параметр также очень ограничен в том, где могут храниться данные. Например, все должно идти в файл в файловой системе. Если кто-то захочет сохранить данные в универсальном OutputStream, необходимо написать совершенно другую реализацию, поэтому структура не будет очень гибкой.
Поэтому я предлагаю, если у вас нет очень веских причин для того, как вы реализуете хранилище (удобство вызова одного метода не в этом, это может быть достигнуто и с другими проектами), я предлагаю вам переместить механизм хранилища в отдельный слой персистентности и реализовать сериализацию, написав TypeAdapter
каждому классу, который вы или кто-либо другой, использующий механизм хранения, храните в системе. Скорее всего, для большинства данных можно использовать адаптеры TypeAdapter по умолчанию, которые поставляются с Gson, но для сложных типов необходимо предоставить способ настраиваемой сериализации. И если ваши объекты данных изменяются, вы должны иметь возможность обеспечить преобразование из уже несовместимого формата хранения в модель данных.
Я ценю проницательный ответ. Я действительно нашел рабочее решение, которое, как мне кажется, может лучше объяснить то, что я искал. На самом деле это то, к чему я стремился — я хотел, чтобы объект Saveable не имел никаких сведений об объектах, которые ему необходимо сохранить, что позволяло любому объекту, расширяющему Saveable, просто запускать .save () и .load () без каких-либо знаний об объектах. высшие классы (ответственность только за сохранение и загрузку). GSON обрабатывает все типы разговоров и карт. См. Мой ответ stackoverflow о том, о чем я говорю, я обновлю его, чтобы включить пример использования.
— FireController 1847