Лекарство от сломанной обратной совместимости

Фронтендеры любят жаловаться, что бекендеры часто ломают обратную совместимость. Вот, мол, описывают структуры данных, АПИ, мы по нему привязываем интерфейс, а через месяц структура меняется, и ничего не работает. Сегодня расскажу, как немного облегчить себе жизнь в таких ситуациях.

Шаблоны спешат на помощь

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

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

Пример

Возьмём абстрактное приложение. У него есть состояние state, где в поле user хранится информация о пользователе.

class State {
  constructor(initialState) {
    this.state = {...initialState}
  }

  update(key, value) {
    this.state = {
      ...this.state,
      [key]: value
    }
  }

  get(key) {
    return this.state[key]
  }
}

const state = new State({user: {}})

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

fetch('/fetch/user.json')
  .then((response) =>
    response.json())
  .then((user) =>
    state.update('user', user))
  .catch(handleError)

Допустим пользователь в приложении описывается таким объектом:

{
  name: 'John',
  lastName: 'Doe',
  birthYear: 1981,
  city: 'Berlin'
}

Пока с сервера приходит подобная структура, наше приложение работает без сбоев. Но вдруг сервер начинает отдавать данные с такой структурой:

{
  fullName: {
    name: 'John',
    lastName: 'Doe'
  },
  birthDate: {
    year: 1981
  },
  address: {
    city: 'Berlin',
    street: '1 Hasselhoff Lane'
  }
}

Если приложение живёт давно, то к полям user.name, user.birthYear, user.city уже привязаны какие‑то его части. Их может быть несколько, и править каждое — не вариант.

Как‑то подгонять структуру ответа в обработчике запроса тоже плохо. Информация о пользователе может запрашиваться не только здесь. Да и код обработчика запроса распухнет.

Поэтому лучше работать со структурой ответа где‑то в другом месте. Напишем адаптер:

class UserToStateAdapter {
  constructor(state) {
    this.state = state
  }

  update(serviceUser) {
    const {fullName, birthDate, address} = serviceUser
    const {name, lastName} = fullName
    const {year} = birthDate
    const {city} = address

    const clientUser = {
      name, 
      lastName, 
      birthYear:year, 
      city,
      address,
    } 

    this.state.update('user', clientUser)
  }
}

const userToStateAdapter = new UserToStateAdapter(state)

Используем:

fetch('/fetch/user.json')
  .then((response) =>
    response.json())
  .then((user) =>
    userToStateAdapter.update(user)
  .catch(handleError)

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

В жизни

Адаптер может пригодиться не только, когда поломали обратную совместимость, но и когда из плоской структуры надо сделать сложную.

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

Начальная структура данных Первый вариант дерева Второй вариант дерева

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

Но есть пара минусов

Например:

  • добавляется ещё одна абстракция в копилку проекта, из‑за этого увеличивается сложность;
  • при создании нового адаптера нужно найти все места, где требуется его вызвать.

Второй пункт проблематичен, если приложение большое. Хотя такие моменты лучше продумывать ещё на этапе проектирования архитектуры.

Чем хорош и когда использовать

Адаптер помогает избежать дублирования кода и множественных исправлений. Позволяет не переписывать существующую логику на клиенте, если на сервере вдруг что‑то поменялось (по крайней мере не сразу). Даёт возможность чаще использовать плоские структуры данных, а сложные создавать только по мере необходимости.

Да, если новая структура в корне отличается от старой, то одним адаптером, скорее всего, уже не обойтись. Но для задач типа «опять они все поля переназвали» — самое то.

Почитать на тему:

← «Избранные произведения Леонардо да Винчи» Ссылки №12 →