Общий класс для GSON LinkedHashMap

Первоначально я разместил это в 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 и Saveable), но я также не могу использовать интерфейсы для объекта Saveable.

Ранее я пробовал следующее, используя классы 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 ответ
1

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

Лично я даже не хочу видеть аннотации, относящиеся к определенному механизму хранения в объектах данных (и я знаю, что это уровень фундаментализма, которого не разделяют многие люди). Почему объект данных должен знать о механизме хранения? Данные не должны знать, как и где они хранятся. Это ответственность уровня сохраняемости. И как только слой сохраняемости изменится на новейший модный формат сериализации, все объекты данных должны быть изменены, даже если их основное назначение и ответственность немного не изменились.

Потому что твой ISaveable интерфейс требует Path Параметр также очень ограничен в том, где могут храниться данные. Например, все должно идти в файл в файловой системе. Если кто-то захочет сохранить данные в универсальном OutputStream, необходимо написать совершенно другую реализацию, поэтому структура не будет очень гибкой.

Поэтому я предлагаю, если у вас нет очень веских причин для того, как вы реализуете хранилище (удобство вызова одного метода не в этом, это может быть достигнуто и с другими проектами), я предлагаю вам переместить механизм хранилища в отдельный слой персистентности и реализовать сериализацию, написав TypeAdapter каждому классу, который вы или кто-либо другой, использующий механизм хранения, храните в системе. Скорее всего, для большинства данных можно использовать адаптеры TypeAdapter по умолчанию, которые поставляются с Gson, но для сложных типов необходимо предоставить способ настраиваемой сериализации. И если ваши объекты данных изменяются, вы должны иметь возможность обеспечить преобразование из уже несовместимого формата хранения в модель данных.

  • Я ценю проницательный ответ. Я действительно нашел рабочее решение, которое, как мне кажется, может лучше объяснить то, что я искал. На самом деле это то, к чему я стремился — я хотел, чтобы объект Saveable не имел никаких сведений об объектах, которые ему необходимо сохранить, что позволяло любому объекту, расширяющему Saveable, просто запускать .save () и .load () без каких-либо знаний об объектах. высшие классы (ответственность только за сохранение и загрузку). GSON обрабатывает все типы разговоров и карт. См. Мой ответ stackoverflow о том, о чем я говорю, я обновлю его, чтобы включить пример использования.

    — FireController 1847

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *