При создании клиент-серверного приложения клиент и сервер должны договориться о том, как взаимодействовать друг с другом. Например, при отправке JSON клиент и сервер должны согласовать имена полей и типы данных. Для баз данных концепция аналогична; без схемы единственной информацией, которую вы могли бы получить, был бы упорядоченный пакет ценностей. Ценности бессмысленны без контекста.

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

Наивный подход к развитию схемы заключался бы в простом внесении произвольных изменений и обновлении всего сразу. Это непрактично для производственных приложений, если вы не согласны с тем, что ваш продукт ломается. Допустим, вы внесли несовместимое изменение, например переименовали столбец в базе данных с productNum на productId. После переноса базы данных на «productId» существующее приложение по-прежнему будет искать «productNum». Когда он не находит столбец с таким именем, он прерывается.

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

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

Этот пост посвящен разнице между ними.

Подготовка сцены

Вот несколько терминов, с которыми вы можете ознакомиться:

Схема - определение типов данных и любого контекста, необходимого для их понимания. Схемы не зависят от того, как кодируются данные, поскольку возможны несколько вариантов сериализации (JSON, двоичный и т. Д.). Схемы также могут иметь версии, что очень важно для понимания обратной и прямой совместимости.

Читатель - служба, анализирующая данные. В случае приложения клиент-сервер это клиент всякий раз, когда сервер отправляет некоторые интересные данные. Однако, когда речь идет о том, какие данные клиент отправляет серверу (например, входные аргументы функции), роли меняются местами: клиент становится писателем.

Writer - служба, создающая данные. В случае приложения клиент-сервер это сервер, но, как и раньше, иногда роли меняются местами.

Для баз данных модуль записи - это служба, которая изначально сохранила строку в базе данных, а модуль чтения - это служба, которая ее извлекает.

Одна диаграмма, которую следует запомнить:

Эта диаграмма полностью объясняет разницу между обратной и прямой совместимостью.

  • Обратная совместимость означает, что читатели с более новой схемой могут правильно анализировать данные от писателей с более старой схемой.
  • Прямая совместимость означает, что читатели с более старой схемой могут правильно анализировать данные от писателей с более новой схемой.

Обратная совместимость

Обратная совместимость важна, потому что:

  • В случае входных параметров: вы можете обновлять серверы без обновления клиентов.
  • Для возвращаемых типов: вы можете обновлять клиентов, не обновляя серверы.
  • Для баз данных: вы не столкнетесь с потерей данных (без обратной совместимости вы не сможете читать данные, записанные более старой версией)

Вот неполный список обратно-совместимых изменений для JSON:

  • Добавление поля со значением по умолчанию. Более старые авторы не будут знать об этом поле, поэтому вместо него будет использоваться значение по умолчанию.
  • Добавление необязательного поля. Более старые авторы не будут знать об этом поле, поэтому вместо него будет использоваться null.
  • Расширение числового типа (например, int до float). Старые авторы всегда будут использовать целые числа, которые являются подмножеством чисел с плавающей запятой.
  • Добавление значения в строку перечисления. Старые авторы просто будут использовать одну из существующих строк перечисления.
  • Удаление поля. Новые читатели проигнорируют все, что было написано ранее в этой области. (Примечание: это не относится ко многим форматам двоичной сериализации!)

Прямая совместимость

Прямая совместимость важна, потому что:

  • В случае входных параметров: вы можете обновлять клиенты, не обновляя серверы.
  • Для типов возврата: вы можете обновлять серверы, не обновляя клиентов.
  • Для баз данных: вы можете запустить миграцию схемы перед развертыванием нового кода, чтобы прочитать его.

Для JSON вот неполный список изменений с прямой совместимостью:

  • Добавление нового обязательного поля. Старшие читатели просто проигнорируют это.
  • Сужение числового типа (например, float до int). Более старые читатели предполагают, что целые числа являются подмножеством чисел с плавающей запятой.
  • Удаление значения из строки перечисления. Старые читатели могут обрабатывать все перечисления.
  • Добавление значения в строку перечисления тогда и только тогда, когда читатель реализовал правильный случай «else». (См. Примечание о перечислениях)

Полная совместимость

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

Для JSON вот неполный список полностью совместимых изменений (некоторые повторяются сверху):

  • Добавление поля со значением по умолчанию
  • Добавление необязательного поля
  • Добавление значения в строку перечисления тогда и только тогда, когда читатель реализовал правильный случай «else». (См. Примечание о перечислениях)

Несовместимость

Если изменение не ни ни в прямом, ни в обратном направлении, оно является несовместимым.

Вот неполный список несовместимых изменений для JSON:

  • Переименование поля
  • Изменение типа поля (кроме числовых преобразований, упомянутых выше)

[Специальное примечание о перечислениях]

Важно писать код таким образом, чтобы можно было вводить новые перечисления. Например, представьте, что вы написали этот код:

if process.status == “STARTED”:
    print(“The process has started”)
else: # assumes process.status == “FINISHED”
    print(“The process hasfinished”)

Теперь, если бы был добавлен новый статус «ОТМЕНЕН», этот код был бы неверным, так как он напечатал бы, что задание завершено, хотя это не так. Вместо этого рассмотрите этот код:

if process.status == “STARTED”:
    print(“The process has started”)
elif process.status == "FINISHED":
    print(“The process hasfinished”)
else:
    raise ValueError("Unexpected status: " + process.status)

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

Заключение

Для клиентов и серверов:

  • Обратная совместимость на сервере гарантирует, что старые клиенты по-прежнему могут анализировать возвращаемые вами результаты.
  • Прямая совместимость гарантирует, что старые клиенты по-прежнему могут вызывать ваши методы

Для баз данных:

  • Обратная совместимость важна, если вы не хотите изменять существующие данные.
  • Прямая совместимость - хороший бонус, но не обязательный, если вы контролируете всех читателей.

При изменении схемы задайте себе следующие вопросы:

  • Это изменение должно быть обратно совместимо? Вы уверены?
    (Большинство изменений, которые появляются на практике, должны быть, по крайней мере, обратно совместимы)
  • Требуется ли это изменение для прямой совместимости? Если нет, уверены ли вы, что все ваши читатели узнают об изменении?

И никогда не забывайте: самое безопасное изменение схемы - вообще не менять.

Дальнейшее чтение

Совместимость - это огромная тема. Чтобы статья была краткой, я упустил много деталей. Есть еще одна еще более обширная тема совместимости кода, которая еще более сложна. (Например, почему переименование параметра в Java является совместимым изменением, а в Python - несовместимым?)

По большей части мое понимание эволюции схемы было получено несколько лет назад в LinkedIn. Вот две статьи, написанные бывшими инженерами LinkedIn: