Архитектурный подход для сохранения данных приложения
Доброго времени суток.
Задача: имеется приложение под Desctop и Android. Нужно определить архитектурный подход для сохранения данных приложения в постоянную память с учетом следующих особенностей:
- Часть полей объекта некоторого типа являются частью его внутренней реализации и должны быть полностью скрыты от клиентского кода, при этом значение этих полей также должны быть сохранены.
- Место и способ хранения объектов могут меняться и добавляться походу портирования и развития приложения (так объекты могут сохраняться в json или xml файлы хранящиеся на машине пользователя с помощью SAX или DOM парсера [Desctop версия]; сохраняться в реляционную[SQLite, HyperSql, MySql] подключаясь через JDBC или какую-нибудь ORM; сохраняться на сервер).
- Объекты некоторых типов неизменяемые.
Какие подходы я пробовал:
Реализовать методы save() и load() у каждого класса, объекты которого нужно сохранять. Если нужно сохранять объект несколькими способами, то для каждого способа делаем свою пару методов (например saveToJson(), loadToJson() и т.д.)
Плюсы на мой взгляд: поля для внутренней реализации остаются полностью скрытыми от клиентского кода и при этом мы спокойно можем их сохранять/загружать.
Минусы на мой взгляд: как мне кажется, нарушается принцип единой ответственности. И второй минус - этот подход нельзя применить к неизменяемым полям (и как следствие - неизменяемым объектам), поскольку их инициализация может проводиться только в момент вызова конструктора (хотелось бы сохранять все объекты приложения единообразным способом).
Использования несколько измененного паттерна Memento (для сохранения объекта получаем его объект-хранитель и сохраняем его. В отличие от классической реализации паттерна, для восстановления объекта-создателя будем передавать объект-хранитель в конструктор, а не в специальный метод).
Плюсы на мой взгляд: читать/писать значение скрытых полей непосредственно у самих объектов - неполучится. Второй плюс - код отвечающий за сохранение можно вынести в отдельный модуль и поместить в отдельном пакете.
Минусы на мой взгляд: объект-хранитель должен открыть доступ к своему состоянию не только для объекта-создателя, но и для объектов, которые будут заниматься его сохранением. Получается, что через объект-хранитель, объект-создатель косвенно раскрывает все свое состояние.
В отдельном модуле объявить DAO интерфейс для каждого типа объектов, которые требуется сохранять. Если у какого-то класса есть поля, доступ к которым он не хотел бы предоставлять клиентскому коду, то DAO интерфейс реализуется в виде вложенного класса.
Плюсы на мой взгляд: благодаря вложенным класса поля для внутренней реализации остаются полностью скрытыми от клиентского кода.
Минусы на мой взгляд: из-за вложенных классов код отвечающий за сохранение не может быть целиком выделен в отдельный модуль и размещен в отдельном пакете. Таким образом код отвечающий за сохранение растаскивается по модулям и пакетам.
Вопрос: Как принято решать похожие задачи? Стоит ли выбрать один из трех вариантов, которые я рассмотрел и если да, то почему? Если я, по вашему мнению, ошибся в описании плюсов и минусов некоторых рассмотренных мной решений - пожалуйста укажите на это.
Ответы (1 шт):
В начале я попытался доработать идею с методами save() и load(). Решил создать некоторые обобщенные интерфейсы Saver и Loader, через которые можно будет единообразно работать с JSON, XML и SQL. Сохраняемые объекты должны иметь метод save(Saver saver) и конструктор с параметром Loader (не метод load(Loader loader), а именно конструктор, чтобы была возможность загружать final поля). Чтобы интерфейсы не требовали раскрытия скрываемых полей сохраняемых объектов и могли применяться для работы с любым типом объектов, а также могли работать с XML, JSON и SQL, необходимо чтобы они сразу были рассчитаны для работы с иерархическими структурами данных. Я решил сделать что-то вроде:
public interface Saver {
public Saver beginSection() throws Exception;
public Saver endSection() throws Exception;
public Saver meta(MetaData meta) throws Exception;
public Saver value(String key, int value) throws Exception;
public Saver value(String key, boolean value) throws Exception;
//... Перегрузки метода value() для других типов данных
public void save() throws Exception;
public void close() throws Exception;
}
и для загрузки
public interface Loader {
public void beginSection() throws Exception;
public void endSection() throws Exception;
public MeatData nextMeta() throws Exception;
public boolean hasNextTokenInSection() throws Exception;
public int getInt(String key) throws Exception;
public boolean getBoolean(String key) throws Exception;
//... Другие методы nextXXX() для других типов данных.
public void close() throws Exception;
}
Но попытка подобрать обобщенный интерфейс оборачивалась либо крайне скудным на функционал интерфейсом не позволяющим использовать особенности формата хранения данных (XML, JSON, таблицы реляционных БД), либо становился крайне противоречивым и раздутым. К тому же подобрать обобщенный интерфейс для всех способов работы с данными просто не получится (я не представляю, КАК должен выглядеть интерфейс, для работы и с ORM, и с XML, и с JSON). вывод - попытка подобрать обобщенный интерфейс для всего на свете - идея изначально обреченная на провал. С каждым форматом хранения данных нужно работать специфичным для него способом.
Как итог, я остановился на втором способе решения поставленной задачи (паттерн Memento), поскольку он позволяет работать с каждым форматом хранения данных специфичным для него способом (не смешивая все в кашу), а также полностью оделить код отвечающий за сохранение, от кода бизнес логики, хотя и ценой ослабления инкапсуляции.