среда, 2 октября 2013 г.

Сериализация и десериализация объектов

Программисты пишут код для того, чтобы решить какую-то задачу. Тот или другой способ они выбирают в зависимости от проблем, которые приходится преодолевать на пути к решению. Чаще всего проблемы видны сразу:

  • "...мы планируем обслуживать 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);
    }

Итак, что мы получили:

  1. Вся логика сериализации и десериализации класса (вместе с "волшебными строками" названий полей) остаётся в одном месте - в самом сериализуемом классе. 
  2. Никакой дополнительной инфраструктуры: фабрик, адаптеров, фасадов и т.п. 
  3. И просто - хорошее упражнение в ООП :)


4 комментария:

  1. GSON - хорошее решение для сериализации "тонких" моделей, т.е. таких классов, которые ничего кроме полей, getter-ов и setter-ов не содержат и никаких преобразований при сериализации/десериализации не выполняют. Кроме того, в статье json как вариант хранения данных взят исключительно в качестве примера, описанная схема легко позволяет сериализовать объекты в базу, в XML, текстовые файлы и т.п. GSON - увы, нет. И не забываем о том, что GSON использует reflection, а этот механизм в разы медленнее прямых вызовов, что в ряде случаев критично.
    В целом, если только json, "тонкие" модели, и скорость работы не важна, да. В других случаях "простой" путь не является лучшим.

    ОтветитьУдалить
  2. Этот комментарий был удален автором.

    ОтветитьУдалить
  3. Мда, неплохая статья, но старовата - хотелось бы понять, как изменилось положение с учётом Java SE 8 - может быть, теперь эту задачу решить несколько проще?..

    ОтветитьУдалить