Статьи

Инкапсуляция разрывов сериализации

Недавно я наткнулся на интересную статью, в которой автор, еще один блоггер-энтузиаст, предлагает набор интересных функций, которые мы все хотели бы иметь в Java, такие как литералы коллекций, поддержка кортежей и записей и многое другое. Я согласен с большинством предложенных идей, так как мне тоже хотелось бы видеть поддержку большинства из них в Java. Однако я также нашел несколько идей, которые я бы посчитал более спорными. Одной из таких идей была идея сделать все объекты Java сериализуемыми по умолчанию. В прошлом я сталкивался с трудностями, связанными с развитием сериализуемых классов, и поэтому я не хотел считать это хорошим предложением. Итак, с единственной целью стимулирования здоровой дискуссии на эту тему я решил написать этот пост.

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

С помощью инкапсуляции мы делаем вид, что ничего не раскрывается во внутреннем представлении объекта, и мы взаимодействуем с нашими компонентами только через их открытые интерфейсы; желательный атрибут, который мы обычно используем позже, когда мы хотим изменить внутреннее представление данных в компоненте, не нарушая никакого кода от его пользователей.

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

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

Рассмотрим для примера, что у нас был класс Person следующим образом:

public class Person {
   private String firstName;
   private String lastName;
   private boolean isMale;
   private int age;
 
  public boolean isMale() {
     return this.isMale;
  }
  public int getAge() {
    return this.age;
  }
  //more getters and setters
}

Допустим, мы выпустили нашу первую версию нашего API с этой абстракцией Person. Для второй версии, однако, мы хотели бы внести два изменения: во-первых, мы обнаружили, что было бы лучше, если бы мы могли хранить дату рождения человека вместо возраста как целое число, а во-вторых, наше определение Возможно, класс Person произошел, когда в Java не было перечислений, но теперь мы хотели бы использовать их для представления пола человека.

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

public class Person {
   private String firstName;
   private String lastName;
   private Gender gender;
   private Date dateOfBirth;
 
  public boolean isMale() {
     return this.gender == Gender.MALE;
  }
  public int getAge() {
    Calendar today = Calendar.getInstance();
    Calendar birth = Calendar.getInstance();
    birth.setTime(this.dateOfBirth);
    return today.get(Calendar.YEAR) - birth.get(Calendar.YEAR);
  }
  //the rest of getters and setters
}

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

Тем не менее, учтите, что класс Person был сериализуем по умолчанию, и если наша система является открытой системой, там могут быть тысячи строк кода, полагаясь на тот факт, что они будут способны воскрешать сериализованные объекты на основе исходного класса, или, может быть, даже клиенты, которые сериализовали расширенные классы на основе исходной версии класса как своего родителя. Некоторые из этих объектов могли быть сериализованы в двоичную форму или какой-либо другой формат пользователями нашего API, которые сейчас хотели бы перейти к нашей второй версии кода.

Затем, если бы мы хотели внести некоторые изменения, как мы это делали во втором примере, мы бы сразу же нарушили некоторые из них; все те, у кого есть сериализованные объекты на основе исходной версии класса, которые хранят объекты, содержащие поле с именем agetype int, содержащее возраст человека, и поле с именем isMaletype, booleanсодержащее информацию о поле, могут потерпеть неудачу во время десериализации этих объекты, потому что новое определение класса использует новые поля и новые типы данных.

Очевидно, что наша проблема здесь заключается в том, что сериализация раскрыла конфиденциальную информацию о наших объектах, и теперь мы не можем просто ничего изменить, даже то, что мы думали, что было инкапсулировано, потому что благодаря сериализации все было открыто открыто.

Теперь рассмотрим сценарий, в котором каждый отдельный класс в API JDK по умолчанию сериализуем. Разработчики Java просто не могли развивать API Java, не рискуя сломать многие приложения. Они будут вынуждены предположить, что кто-то может иметь сериализованную версию любого из классов в JDK.

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