Бизнес-сущности и redux-стейт

Илья Малявин, Яндекс

Бизнес-сущности и redux-стейт

Маркет

Илья Малявин, Разработчик интерфейсов

О чем поговорим?

  1. Зачем нужны бизнес-сущности
  2. Как мы работали со стейтом раньше
  3. Подход в стиле redux
  4. Наш подход с виджетами в Маркете

Вводная

Продаем топоры

У нас есть топоры разных моделей, цен и несколько партнеров-поставщиков

Back to 2000

Как бы мы это сделали в середине прошлого десятилетия?

  1. Какой-нибудь декларативный шаблонизатор (f.g. XSLT)
  2. Древовидный формат данных

Представим «стейт»

или то, что мы отдаем шаблонизатору для первоначального рендеринга

[{
    title: 'Axe 2000',
    offers: [{
        price: 500,
        shop: {
            title: 'Тверские топоры'
        }
    }, {
        price: 200,
        shop: {
            title: 'Новгородские топоры'
        }
    }]
}];

Этого достаточно, чтобы отрисовать страницу

Но тут есть проблемы:

  1. Они не переиспользуются, их структура ничем не гарантируется
  2. К такому коду нельзя написать надежный селектор
  3. Эти данные описывают макет, а не предметную область
  4. С новым макетом или страницей придется изобретать новый формат

Проектируем нормально

О чем следовало подумать в первую очередь?

  1. Какие сущности могут описать предметную область?
  2. Как описать связи между этими сущностями?
  3. Как их хранить и как с ними работать?

Выделим сущности

  1. Модель топора (model)
  2. Конкретное предложение (offer)
  3. Магазин-партнер (shop)

Пометим сущности полем entity

[{
    title: 'Axe 2000',
    entity: 'model',
    offers: [{
        entity: 'offer',
        price: 500,
        shop: {
            entity: 'shop',
            id: "tverShop",
            title: 'Тверские топоры'
        }
    }, {
        entity: 'offer',
        price: 200,
        shop: {
            entity: 'shop',
            id: "novgShop",
            title: 'Новгородские топоры'
        }
    }]
}];

Почему стало лучшие?

  1. Работая с бизнес-сущностями, мы можем поддерживать контракт
  2. Сущности остаются прежними вне зависимости от макета
  3. Легко и понятно, как написать селекторы, на что завязаться

View data vs entities

View data

Entities

Однако есть и другие проблемы

  1. Дублирование данных
  2. Нет единого источника истины
  3. Такие данные сложно изменять

Пример с дублированием данных

[{
    title: 'Axe 2000',
    entity: 'model',
    offers: [{
        entity: 'offer',
        price: 500,
        shop: {
            entity: 'shop',
            id: "tverShop",
            title: 'Тверские топоры'
        }
    }, {
        entity: 'offer',
        price: 200,
        shop: {
            entity: 'shop',
            id: "tverShop",
            title: 'Тверские топоры'
        }
    }]
}];

Наше время

Redux и normalizr во фронтенде

Что это значит?

Redux подталкивает нас к нормализации:

Подробнее можно почитать в документации: https://redux.js.org/recipes/structuringreducers/normalizingstateshape

А что такое normalizr?

Небольшая библиотека для конвертации вложенных данных в нормализованные на основе описанной схемы

https://github.com/paularmstrong/normalizr

Пример нормализованных данных

{
    items: [{
        entity: 'offer',
        id: 1
    }],
    entities: {
        offer: {
            "1": {
                entity: 'offer',
                price: 500,
                shop: "tverShop",
            },
        },
        shop: {
           "tverShop": {
                entity: 'shop',
                title: "Тверские топоры"
            },
        },
    },
}

Итого

Усложним задачу

Теперь менеджеры магазина хотят:

img

Что это значит с точки зрения разработки?

Другими словами, набор топоров становится виджетом

Что такое виджеты?

Это независимая часть приложения, которая:

Звучит здорово, однако…

как же дедупликация данных?

как обеспечить независимость виджетов в store?

Наше решение

Два пункта

  1. Разделение данных на данные виджета и коллекции
  2. Своя версия connect

Коллекции

  1. Неупорядоченное хранилище сущностей по ключу
  2. Доступно всем виджетам сразу
{
    collections: {
	    offer: {
            1: {id: "1", price: 500, shop: "tverShop"},
            2: {id: "2", price: 233, shop: "tverShop"},
            3: {id: "3", price: 600, shop: "tverShop"},
            4: {id: "4", price: 350, shop: "tverShop"},
        },
        shops: {"tverShop": {title: 'Новгородские топоры'}}
	}
}

Данные виджета

  1. Ключи для коллекций с сохранением нужного порядка
  2. Данные, которые не являются сущностями и принадлежат именно этому виджету
{
	widgets: {
	    "axesPack1": {
	        offerIds: [1, 2, 3],
	    },
        "axesPack2": {
            offerIds: [2, 3, 4],
	    },
	},
}

Соберем все вместе

{
    collections: {
	    offer: {
            1: {id: "1", price: 500, shop: "tverShop"},
            2: {id: "2", price: 233, shop: "tverShop"},
            3: {id: "3", price: 600, shop: "tverShop"},
            4: {id: "4", price: 350, shop: "tverShop"},
        },
        shops: {"tverShop": {title: 'Новгородские топоры'}}
	}
	widgets: {
	    "axesPack1": {
	        offerIds: [1, 2, 3],
            title: 'Топоры по лучшим ценам',
	    },
        "axesPack2": {
            offerIds: [2, 3, 4],
            title: 'Топоры специально для вас',
	    },
	},
}

Connect

Чтобы гарантировать принцип независимости, используем свою обертку над connect

  1. Ограничивает mapStateToProps коллекциями и данными виджета
  2. В остальном работает точно так же, как и обычный connect
function mapStateToProps(widgetData, collections) {
    // Берем id нужных нам топоров
    const {offerIds} = widgetData;

    return {
        // Для каждого id получаем сущность из коллекции
        axes: offerIds.map(id => collections.offer[id]),
    };
}

Если визуализировать

Хорошо, а что подгрузкой данных?

  1. Виджеты сами знают, как сделать запрос за дополнительными данными
  2. Виджеты сами могут обновить коллекции (это просто редюсер)

Подведем итог

  1. Всегда следует не адаптировать данные под макеты, а описывать бизнес-сущности
  2. Redux позволяет дедуплецировать и легко обновлять данные
  3. На его основе можно удобно сконструировать свою виджетную систему

PS

Детальный рассказ про нашу виджетную систему от Паши Павелко:

http://www.highload.ru/siberia/2018/abstracts/3682

Контакты

Илья Малявин

Разработчик интерфейсов