- "...мы планируем обслуживать 10 миллионов клиентов к концу первого года." - Пишем с оглядкой на масштабирование.
- "...нужно быстро внедрять 100500 похожих фич." - Думаем над системой плагинов.
- И т.п.
Однако есть проблемы, которые вроде как и не заметны поначалу. Но потом, спустя год активной доработки проекта мы вдруг увязаем в них как в болоте. Одну такую я встретил недавно и цепочка моих рассуждений при её решении показалась мне достаточно интересной, чтобы оформить её тут. В конце этой цепочки - довольно элегантное на мой взгляд решение, которое позволит реализовать специфическую сериализацию объектов в вашем проекте. Это не открытие, подобный подход используется, например, в Parcelable в Android. Но паттерн, до которого дошёл "от проблемы" а не путём изучения научных трудов, запоминается намного лучше.
И, кстати, если вы пришли сюда скопировать исходничек не вникая в текст, лучше почитайте другие посты. Тут много букв, картинок и вообще, скукотища ;)
И, кстати, если вы пришли сюда скопировать исходничек не вникая в текст, лучше почитайте другие посты. Тут много букв, картинок и вообще, скукотища ;)
Итак, в чём же проблема?
Допустим вы пишите проект, в котором часть ваших объектов будет храниться в сериализованном виде. 10 из 9 моих проектов делаются так. К примеру мы пишем файлообменник и для кеширования используем Redis. У нас есть объект User и File, первый из которых описывает атрибуты пользователя, второй - загруженного им файла. Допустим, набор полей у них следующий:
Для того, чтобы упростить рутинную задачу записи в кэш и чтения оттуда мы определили в обоих классах методы toJson() и fromJson(String json). Первый возвращает строку, второй принимает строку и возвращает объект. Казалось бы всё хорошо.
Потом появляется необходимость хранить сообщения для пользователей и с ними нужно бы поступить также. Но ваш стажёр не стал следовать общей схеме и написал код, выполняющий сериализацию прямо в методе, который пишет в кэш. Как его заставить соблюдать порядок? У нас ведь OOП!
Запилим интерфейс?
Интерфейс назовём Jsonable и заставим классы всех сериализуемых объектов реализовать определённые в нём методы. Один, понятное дело, public String toJson(). А второй, по идее должен быть статическим, чтобы порождать объект или вообще, конструктором. Вот тут и возникает первая проблема: в интерфейсах нельзя декларировать конструктор или статический метод. Какой же выход? На ум сразу приходит паттерн factory method, с помощью которого можно соорудить что-то вроде такого:
Не забываем, что логика десериализации у наших объектов может отличаться не только в том, что число и имена полей у них различны. Да и юзать reflection тут наверное, всё-таки неправильно. Мы же о кешировании говорим, верно? Быстродействие и экономия памяти приветствуются. Итого, получаем такой вариант:
Так жить можно, но для каждого сериализуемого класса в проекте вам придётся часть его логики вписывать в фабрику. И если структура классов поменялась - опять править её же. Неудобно и багоопасно. Вот если бы фабрику "растащить" по самим сериализуемым классам, то мы бы получили код поудобнее.
Растаскиваем фабрику
Статический метод у интерфейса определить нельзя. Но можно у абстрактного класса. Это пока нас никуда не ведёт, ведь статический метод не может быть абстрактным. Впрочем, что нам мешает его реализовать прямо тут в абстрактном классе? Как, спросите вы, ведь логика десериализации отличается у наследников!
А вот так:
public static Jsonable fromJson(Creator creator, String json) { return creator.create(json); }
Стоп, а что такое Creator? А всё очень просто. Это интерфейс, вложенный в наш абстрактный класс. Такой подход как бы намекает, что наследники должны реализовать его, если они хотят быть восстановлены из String стандартным образом. Нестандартный легко запретить, например, делая у Jsonable приватный конструктор.
Что в целом?
public abstract class Jsonable { protected Jsonable(){} public abstract String toJson(); public static Jsonable fromJson(Creator creator, String json) { return creator.create(json); } public interface Creator { public abstract Jsonable create(String json); } }
Это наследник:
public class User extends Jsonable { private String userId, login, status; @Override public String toJson() { JSONObject jo = new JSONObject(); try { jo.put("userId", userId); jo.put("login", login); jo.put("status", status); } catch (JSONException e) { e.printStackTrace(); } return jo.toString(); } public static class Creator implements Jsonable.Creator { @Override public Jsonable create(String json) { User u = new User(); try { JSONObject jo = new JSONObject(json); u.userId = jo.optString("userId"); u.login = jo.optString("login"); u.status = jo.optString("status"); } catch (JSONException e) { e.printStackTrace(); } return u; } } }
А это - собственно сериализация и десериализация:
public static String user2String(Jsonable obj) { return obj.toJson(); } public static User string2User(String json) { return (User) Jsonable.fromJson(new User.Creator(), json); }
Итак, что мы получили:
- Вся логика сериализации и десериализации класса (вместе с "волшебными строками" названий полей) остаётся в одном месте - в самом сериализуемом классе.
- Никакой дополнительной инфраструктуры: фабрик, адаптеров, фасадов и т.п.
- И просто - хорошее упражнение в ООП :)
эээээ.. Gson ?
ОтветитьУдалитьGSON - хорошее решение для сериализации "тонких" моделей, т.е. таких классов, которые ничего кроме полей, getter-ов и setter-ов не содержат и никаких преобразований при сериализации/десериализации не выполняют. Кроме того, в статье json как вариант хранения данных взят исключительно в качестве примера, описанная схема легко позволяет сериализовать объекты в базу, в XML, текстовые файлы и т.п. GSON - увы, нет. И не забываем о том, что GSON использует reflection, а этот механизм в разы медленнее прямых вызовов, что в ряде случаев критично.
ОтветитьУдалитьВ целом, если только json, "тонкие" модели, и скорость работы не важна, да. В других случаях "простой" путь не является лучшим.
Этот комментарий был удален автором.
ОтветитьУдалитьМда, неплохая статья, но старовата - хотелось бы понять, как изменилось положение с учётом Java SE 8 - может быть, теперь эту задачу решить несколько проще?..
ОтветитьУдалить