Нормализация данных

Данные приложения почти всегда имеют иерархическую структуру. Возьмём для примера список постов на Хекслете. У каждого поста есть автор, комментарии, у которых в свою очередь есть лайки. Эти данные можно представить так:

const posts = [
  {
    question: 'Как писать код?',
    likesCount: 2,
    comments: [
      {
        answer: 'Открой редактор!',
        likesCount: 1,
        createdAt: '11-12-2022',
      },
      {
        answer: 'Сидя!',
        likesCount: 3,
        createdAt: '11-12-2022',
      },
    ]
  },
  {
    question: 'Что лучше: vim или emacs?',
    likesCount: 2,
    comments: [
      {
        answer: 'FAR зе бест!',
        likesCount: 100,
        createdAt: '11-12-2022',
      },
    ]
  }
];

Иерархическое представление данных хорошо отражает их структуру. Сразу видно, кто к чему относится. Их удобно выводить и вполне удобно изменять. Особенно если вывод на экране совпадает с их структурой, и данные между собой не пересекаются. Топики Хекслета как раз такой пример. Каждый топик живёт своей независимой жизнью (кое-какие зависимости есть, но они не касаются самих данных).

Однако если данные связаны, то иерархическая структура превращается в проблему. Представьте себе, что надо выводить 10 последних комментариев. Как это сделать? Придётся ходить по всем топикам, брать все комментарии, объединять и искать самые свежие. Устрашающий пример:

const comments = posts.flatMap((p) => p.comments);
const sortedComments = comments.sort((c1, c2) => new Date(c2.createdAt) - new Date(c1.createdAt));
const bestComments = sortedComments.slice(0, 10);

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

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

Другой способ – нормализовать данные, прямо как в реляционных базах данных. Представить их плоскими массивами. Например так:

// В реальном приложении всё будет храниться в одном объекте состояния

const posts = [
  {
    id: 3,
    question: 'Как писать код?',
    likesCount: 2,
  },
  {
    id: 100
    question: 'Что лучше: vim или emacs?',
    likesCount: 2,
  }
];

const comments = [
  {
    id: 1,
    postId: 3,
    answer: 'Открой редактор!',
    likesCount: 1,
  },
  {
    id: 8,
    postId: 3,
    answer: 'Сидя!',
    likesCount: 3,
  },
  {
    id: 3,
    postId: 100,
    answer: 'FAR зе бест!',
    likesCount: 100,
  },
]

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

Иногда применяют технику обратную нормализации – денормализацию, но это больше про бэкенд, чем фронтенд

Но за всё приходится платить. Упрощая в одном месте, она усложняет в другом. Теперь для извлечения комментариев конкретного топика придётся написать такой код:

const postId = /* идентификатор поста */;
const commentsForPost = comments.filter((c) => c.postId === postId);

Здесь кода не больше, чем при выборке конкретного поста. Но он сложнее в алгоритмическом смысле, на него тратится больше ресурсов. Является это проблемой или нет, вопрос открытый. Как правило, нет. Фронтенд очень редко оперирует большими количествами, например, десятками и сотнями тысяч. Чаще всего размеры коллекций ограничиваются сотней-другой элементов.

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

Мы учим программированию с нуля до стажировки и работы. Попробуйте наш бесплатный курс «Введение в программирование» или полные программы обучения по Javascript, PHP, Python и Java.

Хекслет

Подробнее о том, почему наше обучение работает →