Стан як масив об'єктів проти об'єкта, що вводиться ідентифікатором


94

У главі Проектування форми стану , документи пропонують зберігати свій стан в об'єкті, введеному за допомогою ідентифікатора:

Зберігайте кожну сутність в об’єкті, що зберігається з ідентифікатором як ключем, і використовуйте ідентифікатори для посилання на неї з інших об’єктів або списків.

Вони продовжують констатувати

Подумайте про стан програми як про базу даних.

Я працюю над формою стану для списку фільтрів, деякі з яких будуть відкритими (вони відображаються у спливаючому вікні) або мають вибрані параметри. Коли я прочитав "Подумайте про стан програми як про базу даних", я подумав про те, щоб сприймати їх як відповідь JSON, оскільки вона буде повернута з API (сам підтримується базою даних).

Тож я думав про це як

[{
    id: '1',
    name: 'View',
    open: false,
    options: ['10', '11', '12', '13'],
    selectedOption: ['10'],
    parent: null,
  },
  {
    id: '10',
    name: 'Time & Fees',
    open: false,
    options: ['20', '21', '22', '23', '24'],
    selectedOption: null,
    parent: '1',
  }]

Однак документи пропонують формат, подібний до

{
   1: { 
    name: 'View',
    open: false,
    options: ['10', '11', '12', '13'],
    selectedOption: ['10'],
    parent: null,
  },
  10: {
    name: 'Time & Fees',
    open: false,
    options: ['20', '21', '22', '23', '24'],
    selectedOption: null,
    parent: '1',
  }
}

Теоретично це не має значення, доки дані можна серіалізувати (під заголовком "Держава") .

Тому я щасливо йшов із підходом масиву об’єктів, поки не писав свій редуктор.

За допомогою підходу об’єкт-за-ідентифікатором (та ліберального використання синтаксису поширення) OPEN_FILTERчастина редуктора стає

switch (action.type) {
  case OPEN_FILTER: {
    return { ...state, { ...state[action.id], open: true } }
  }

Тоді як при підході до масиву об’єктів це більш детально (і допоміжна функція, що залежить)

switch (action.type) {
   case OPEN_FILTER: {
      // relies on getFilterById helper function
      const filter = getFilterById(state, action.id);
      const index = state.indexOf(filter);
      return state
        .slice(0, index)
        .concat([{ ...filter, open: true }])
        .concat(state.slice(index + 1));
    }
    ...

Тож мої запитання потрійні:

1) Чи є простота редуктора мотивацією для використання підходу, об’єднаного за допомогою ідентифікатора? Чи існують інші переваги такої форми держави?

і

2) Здається, що підхід, об’єднаний за допомогою ідентифікатора, ускладнює роботу зі стандартним входом / виходом JSON для API. (Ось чому я спочатку пішов із масивом об’єктів.) Отже, якщо ви використовуєте такий підхід, чи просто ви використовуєте функцію для її трансформації між форматами JSON та формами стану? Це здається незграбним. (Хоча, якщо ви відстоюєте такий підхід, це частина ваших міркувань про те, що це менш незграбно, ніж наведений нижче редуктор масиву об’єктів?)

і

3) Я знаю, що Ден Абрамов розробив редукс, щоб теоретично бути агностиком структури даних даних (як пропонується "За згодою", стан верхнього рівня - це об'єкт або якась інша колекція ключ-значення, подібна Map, але технічно це може бути будь-яка тип , " наголос мій). Але з огляду на вищесказане, чи просто "рекомендується" зберігати його як об'єкт, закріплений за допомогою ідентифікатора, чи є інші непередбачені больові точки, на які я зіткнуся, використовуючи масив об'єктів, які роблять це таким, що я повинен просто перервати це планувати і намагатися дотримуватися об’єкта, введеного за допомогою ідентифікатора?


2
Це цікаве питання, і одне у мене також було, лише для того, щоб дати деяке розуміння, хоча я, як правило, нормалізую у redux замість масивів (суто тому, що шукати простіше), я виявляю, що якщо взяти нормалізований підхід, сортування стає проблема, оскільки ви не отримуєте ту саму структуру, яку надає вам масив, тому ви змушені сортувати самі.
Роберт Сондерс,

Я бачу проблему в підході "об’єкт-за-ідентифікатором", однак це не часто, але нам доводиться розглядати цей випадок під час написання будь-якого додаткового інтерфейсу. То що, якщо я хочу змінити порядок сутності за допомогою елемента перетягування, перерахованого як упорядкований список? Зазвичай тут підхід "об'єкт-за-ідентифікатором" зазнає невдачі, і я б, безсумнівно, пішов із масивом об'єктних підходів, щоб уникнути таких щедрих проблем.
Могло

Як можна відсортувати об’єкт, складений із предметів? Це здається неможливим.
Девід Вільгубер,

@DavidVielhuber Ви маєте на увазі, окрім того, що використовуєте щось на зразок лодаша sort_by? const sorted = _.sortBy(collection, 'attribute');
nickcoxdotme

Так. На даний момент ми перетворюємо ці об'єкти в масиви всередині обчислюваної властивості vue
David Vielhuber

Відповіді:


46

Q1: Простота редуктора - це результат того, що не потрібно шукати масив, щоб знайти правильний запис. Відсутність необхідності шукати масив - це перевага. Селектори та інші засоби доступу до даних можуть і часто отримують доступ до цих елементів за допомогою id. Необхідність пошуку в масиві для кожного доступу стає проблемою продуктивності. Коли масиви збільшуються, проблема продуктивності різко погіршується. Крім того, оскільки ваш додаток ускладнюється, показуючи та фільтруючи дані в більшій кількості місць, проблема також погіршується. Поєднання може бути згубним. Отримуючи доступ до елементів id, час доступу змінюється з O(n)на O(1), що для великих n(тут елементів масиву) має величезну різницю.

Q2: Ви можете normalizrдопомогти вам з перетворенням з API на зберігання. Що стосується normalizr V3.1.0, ви можете використовувати denormalize, щоб піти іншим шляхом. Тим не менш, програми часто є більшими споживачами, ніж виробниками даних, і як такий перетворення на зберігання зазвичай здійснюється частіше.

Запитання 3: Проблеми, з якими ви зіткнетеся, використовуючи масив, - це не стільки проблеми з умовами зберігання та / або несумісністю, скільки проблеми з продуктивністю.


нормалізатор - це знову те, що, безсумнівно, створить біль, коли ми змінимо дефс у серверній системі. Тож це має бути в курсі кожного разу
Kunal Navhate

12

Подумайте про стан програми як про базу даних.

Це ключова ідея.

1) Наявність об’єктів з унікальними ідентифікаторами дозволяє вам завжди використовувати цей ідентифікатор при посиланні на об’єкт, тому вам потрібно передавати мінімальну кількість даних між діями та редукторами. Це ефективніше, ніж використання array.find (...). Якщо ви використовуєте підхід до масиву, вам доведеться передати весь об'єкт, і це може дуже швидко заплутатися, ви можете в кінцевому підсумку відтворити об'єкт на різних редукторах, діях або навіть у контейнері (ви цього не хочете). Представлення завжди зможуть отримати повний об’єкт, навіть якщо пов’язаний з ними редуктор містить лише ідентифікатор, оскільки при зіставленні стану ви десь отримаєте колекцію (подання отримує весь стан, щоб зіставити його зі властивостями). Через усе, що я вже сказав, дії в підсумку мають мінімальну кількість параметрів, а редуктори - мінімальну кількість інформації, спробуйте,

2) Підключення до API не повинно впливати на архітектуру вашого сховища та редукторів, тому у вас є дії, щоб тримати розділення проблем. Просто помістіть свою логіку перетворення в API та поза ним у багаторазовий модуль, імпортуйте цей модуль у дії, що використовують API, і це має бути ним.

3) Я використовував масиви для структур з ідентифікаторами, і це непередбачені наслідки, які я зазнав:

  • Постійне відтворення об’єктів у коді
  • Передача непотрібної інформації редукторам та дії
  • Як наслідок цього, поганий, не чистий і не масштабований код.

У підсумку я змінив структуру даних і переписав багато коду. Вас попередили, будь ласка, не потрапляйте в халепу.

Також:

4) Більшість колекцій з ідентифікаторами призначені для використання ідентифікатора як посилання на цілий об’єкт, ви повинні скористатися цим. Виклики API отримають ідентифікатор, а потім решту параметрів, а також ваші дії та редуктори.


Я стикаюся з проблемою, коли ми маємо програму з великою кількістю даних (від 1000 до 10000), що зберігаються ідентифікатором в об’єкті в сховищі redux. У поданнях усі вони використовують сортувані масиви для відображення даних часових рядів. Це означає, що кожного разу, коли робиться рендерінг, він повинен взяти весь об’єкт, перетворити на масив і відсортувати його. Мені було доручено покращити продуктивність програми. Чи є це випадком використання, коли має сенс зберігати дані у відсортованому масиві та використовувати двійковий пошук для видалення та оновлення замість об’єкта?
Вільям Чоу

Мені довелося зробити деякі інші хеш-карти, отримані з цих даних, щоб мінімізувати час обчислень на оновлення. Це змушує оновлення всіх різних поглядів вимагати власної логіки оновлення. До цього всі компоненти забирали об’єкт із сховища та відновлювали структури даних, необхідні для його перегляду. Єдиний спосіб, який я можу придумати, щоб забезпечити мінімальну незграбність в інтерфейсі, - це використання веб-працівника для перетворення об’єкта в масив. Компромісом за це є простіша логіка пошуку та оновлення, оскільки всі компоненти залежать лише від одного типу даних для читання та запису.
Вільям Чоу

8

1) Чи є простота редуктора мотивацією для використання підходу, об’єднаного за допомогою ідентифікатора? Чи існують інші переваги такої форми держави?

Основною причиною того, що ви хочете зберегти сутності в об’єктах, що зберігаються з ідентифікаторами як ключі (також звані нормалізованими ), є те, що працювати з глибоко вкладеними об’єктами справді громіздко (саме це ви зазвичай отримуєте від REST API в більш складному додатку) - як для компонентів, так і для редукторів.

Трохи важко проілюструвати переваги нормалізованого стану на вашому поточному прикладі (оскільки у вас немає глибоко вкладеної структури ). Але припустимо, що параметри (у вашому прикладі) також мали заголовок і були створені користувачами у вашій системі. Завдяки цьому відповідь виглядатиме приблизно так:

[{
  id: 1,
  name: 'View',
  open: false,
  options: [
    {
      id: 10, 
      title: 'Option 10',
      created_by: { 
        id: 1, 
        username: 'thierry' 
      }
    },
    {
      id: 11, 
      title: 'Option 11',
      created_by: { 
        id: 2, 
        username: 'dennis'
      }
    },
    ...
  ],
  selectedOption: ['10'],
  parent: null,
},
...
]

Тепер припустимо, ви хотіли створити компонент, який відображає список усіх користувачів, які створили параметри. Для цього спочатку потрібно запитати всі елементи, потім переглядати кожен з їх варіантів і, нарешті, отримати ім’я created_by.username.

Кращим рішенням буде нормалізація реакції на:

results: [1],
entities: {
  filterItems: {
    1: {
      id: 1,
      name: 'View',
      open: false,
      options: [10, 11],
      selectedOption: [10],
      parent: null
    }
  },
  options: {
    10: {
      id: 10,
      title: 'Option 10',
      created_by: 1
    },
    11: {
      id: 11,
      title: 'Option 11',
      created_by: 2
    }
  },
  optionCreators: {
    1: {
      id: 1,
      username: 'thierry',
    },
    2: {
      id: 2,
      username: 'dennis'
    }
  }
}

Завдяки цій структурі набагато простіше та ефективніше перерахувати всіх користувачів, які створили параметри (ми виділили їх у сутності .optionCreators, тому нам просто потрібно прокрутити цей список).

Також досить просто показати, наприклад, імена користувачів тих, хто створив параметри для елемента фільтра з ідентифікатором 1:

entities
  .filterItems[1].options
  .map(id => entities.options[id])
  .map(option => entities.optionCreators[option.created_by].username)

2) Здається, що підхід, об’єднаний за допомогою ідентифікатора, ускладнює роботу зі стандартним входом / виходом JSON для API. (Ось чому я спочатку пішов із масивом об’єктів.) Отже, якщо ви використовуєте такий підхід, чи просто ви використовуєте функцію для її трансформації між форматами JSON та формами стану? Це здається незграбним. (Хоча, якщо ви відстоюєте такий підхід, це частина ваших міркувань про те, що це менш незграбно, ніж наведений нижче редуктор масиву об’єктів?)

Відповідь JSON можна нормалізувати, використовуючи, наприклад, normalizr .

3) Я знаю, що Ден Абрамов розробив редукс, щоб теоретично бути агностиком структури даних даних (як пропонується "За згодою", стан верхнього рівня - це об'єкт або якась інша колекція ключ-значення, подібна Map, але технічно це може бути будь-яка тип, "наголос мій). Але з огляду на вищесказане, чи просто "рекомендується" зберігати його як об'єкт, закріплений за допомогою ідентифікатора, чи є інші непередбачені больові точки, на які я зіткнуся, використовуючи масив об'єктів, які роблять це таким, що я повинен просто перервати це планувати і намагатися дотримуватися об’єкта, введеного за допомогою ідентифікатора?

Це, мабуть, рекомендація для більш складних додатків з великою кількістю глибоко вкладених відповідей API. У вашому конкретному прикладі це насправді не так важливо.


1
mapповертається невизначеним, як тут , якщо ресурси отримуються окремо, що робить filterшлях занадто складним. Чи є рішення?
Сараванабалагі Рамачандран

1
@tobiasandersen, на вашу думку, нормально для сервера повертати нормалізовані дані, ідеальні для реакції / відновлення, щоб уникнути того, щоб клієнт виконував перетворення за допомогою libs, як normalizr? Іншими словами, змусьте сервер нормалізувати дані, а не клієнт.
Matthew
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.