Сразу извиняюсь за свою манеру повествования, ибо она похоже больше на поток сознания, но главное уловить суть, которую я хочу донести.

Я хочу подготовить небольшой цикл публикаций про работу с данными:

  1. Общий обзор FOR-архитектуры (эта статья)

  2. Взгляд на валидацию данных и частные применения

  3. Разбор FOR со стороны поставщика данных (ключевая часть, нужная для понимания всей картины)

  4. Гомогенность данных в больших распределенных системах (идея, выросшая из валидации и использующая те же механизмы)

Начнем с определения кому надо

Если рассматривать разработку веб приложений, то я бы выделил 2 вектора:

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

Второй - enterprise, это когда есть 1 продукт (или больше, что не важно в данном контексте) и он должен быть сделан на уровне, когда поддержка должна быть простой и легкой десятилетиями.

Если мы рассматриваем студийный подход - то имеем более низкую вовлеченность в предметную область (domain driven) и подходы ограничены более простым и менее проработанным ТЗ (как правило, судя по моему опыту, не пытаюсь сказать, что в студиях пишут хуже, просто там как правила оплата не по часам, а по проектно, а с большими студиями я не сталкивался) а вот в энтерпрайзе уже надо сделать более качественно, потому что нам (писателям кода) и надо будет поддерживать и расширять код под постоянно изменчивый бизнес (и лишь такой бизнес хорошо живет, а не тот, кто может позволить быть в статике).

Данный параграф обозначил аудиторию моего подхода и моих идей, я полагаю не стоит в "быстрой" разработке использовать то, что я предлагаю, потому что это будет сильно дольше и, соответственно, дороже.

Начнем с ситуации. Рассмотрим 2 кейса.

Первый:

  • У вас react/vue/что-то иное

  • Есть некий компонент (форма создания какой-то элемента)

  • Поля формы сильно отличаются от контекста и типа этого элемента (к примеру, товары в магазине)

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

Где-то в дебрях:

let form_data = axios.get(element_type);

По моему видению, это всегда должно быть иначе

let form_data = FormProvider.get_by_type(element_type);

Плюсы очевидны по мне.

  1. Низкая связанность

  2. Не зависимость от поставщика

  3. Легкий рефакторинг (данных)

  4. Мы можем добавить ttl-кеш в наш провайдер (логи, единая точка валидации отправляемых и получаемых данных по роута и многое другое), формы редко меняются. В таком случае форма будет уже мгновенно вызываться

Концепция работы Data Providers на фронте:

На бекенде, это выглядит примерно также:

Итого мы имеем определение Data Provider - это интерфейс работы с данными внутри вашего приложения

И второе нужное нам определение FOR - это аббревиатура filters+options+response

Моё видение реализации

Data Provider имеет 2 механизма работы:

  1. DataProvider::search($filters, $options, $response) статичный метод на входе, который покрывает 80% задач. Если всё в вашей системе представлено в виде элементов - то данный метод даст высший уровень гибкости вашему API, потому что позволит находить что угодно в рамках свои полномочий.

  2. Также мы тут добавляем некий массив примитивных операций со статичным интерфейсом, когда фильтры избыточны, их может быть достаточно много и они также могут скрывать внутри себя фильтры, обеспечивая единообразие и одновременно упрощенный синтаксис.

Примеры:

  • CompanyProvider::get_by_id()

  • JobProvider::get_actual()

Конкретика

Архитектура состоит из 2 частей: пользователь и поставщик.

Поставщика я хочу детальнее описать позже, пока приведу пример реализации, который более нагляден

(код размещен в папке: DataProvider ElementSearch, эта часть будет подробно разобрана в другой статье) https://gitlab.com/dev_docs/software_architecture/for-architecture/-/blob/main/code_examples/DataProvider%20ElementSearch%20%5Bprototype%5D/BaseFilters.php

Концепция его работы:

:

Теперь взгляд со стороны использования:

  • Filters - определяет условия поиска (что и где ищем)

  • Options - определяет область поиска (все что не filter и не response)

  • Response - определяет модификацию / формат ответа (как обработаем ответ)

Примеры запроса будут размещены тут: https://gitlab.com/dev_docs/software_architecture/for-architecture/-/tree/main/code_examples/DataProvider%20ElementSearch%20%5Bprototype%5D/Examples

Реализация внутри системы (back / php)

(код размещен в папке: DataProvider ElementSearch, эта часть будет подробно разобрана в другой статье) https://gitlab.com/dev_docs/software_architecture/for-architecture/-/tree/main/code_examples/DataProvider%20ElementSearch%20%5Bprototype%5D

Фильтр - массив условий, фильтр состоит из:

  • object - объект поиска, состоит из:

  • element_type_id (element_type) - сама сущность (user, sku, company, news, properties ...)

  • element_id - подсущность. к примеру, id свойства или поле, к примеру: user_name или если props, то 69

  • operator_id (operator) - оператор поиска (у меня они все зарегистрированы и есть в виде констант, это лучше, чем их строковое представление) https://gitlab.com/dev_docs/software_architecture/for-architecture/-/blob/main/code_examples/DataProvider%20ElementSearch%20%5Bprototype%5D/Operators.php

  • value - значение поиска (может быть любой тип, все зависит от контекста)

Примеры фильтров:

DataProvider.search(
  {
    filters : 
    [
      {
          object :
          {
              element_type_id : _CONSTANTS.ELEMENT_TYPES.SOME_ELEMENT_TYPE_ID,
              element_id      : 'some_id'
          },
          operator_id : _CONSTANTS.OPERATORS.EQUAL,
          value       : options.element_id
      }
    ]
  }
)
PaymentProvider.search(
{
    filters  :
    [
        {
            object      :
            {
                element_type_id : _CONSTANTS.ELEMENT_TYPES.PAYMENT,
                element_id      : 'payment_id'
            },
            operator_id : _CONSTANTS.OPERATORS.EQUAL,
            value       : this.element_id
        }
    ],
    response :
    {
        structure_mode : 'listing'
    }
})

И немного посложнее

{
  filters :
  [
      {
          object :
          {
              element_type_id   : _CONSTANTS.ELEMENT_TYPES.CATEGORY,
              element_id        : null
          },
          operator_id    : _CONSTANTS.OPERATORS.EQUAL,
          value          : 27
      },
      {
          object :
          {
              element_type_id   : _CONSTANTS.ELEMENT_TYPES.ITEM_STATUS,
              element_id        : null
          },
          operator_id   : _CONSTANTS.OPERATORS.IN,
          value         : [103, 101]
      }
  ],
  response :
  {
      structure_mode : 'listing',
      add :
      {
          relations :
          [
              {
                  relation_id        : 160,    // связь - подчинённые
                  relation_field    : 'master',
                  data_fields        : 'IDS'
              },
              {
                  relation_id        : 125, // связь с должностью
                  relation_field    : 'slave',
                  data_fields        :
                  {
                      general : ['item_full_name']
                  }
              },
              {
                  relation_id        : 163,    // связь с физическим лицом
                  relation_field    : 'slave',
                  data_fields        :
                  {
                      property :
                      [
                          18, // фамилия
                          389 // инициалы
                      ]
                  }
              }
          ]
      }
  }
}

Именно это часть из архитектуры идёт в модель, а затем в БД (если речь про бек) или на апи (если речь про фронт).

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

Response - определяет тот формат (модель, дата-класс, ...) который вы хотите получить или, к примеру, вы хотите получить только ids элементов.

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

Немного рефлексии, для цельности картины:

Основные тезисы, которых точно стоит придерживаться в работе с данными

  1. Абстракции - наше всё

  2. Принцип единой ответственности (в чуть более широкой интерпретации) - должен быть в основе каждого вашего технического и не очень решения

  3. Простота - во всём, простой код - легче понять и развить (исправить)

  4. Бизнес логика и отдельные модули - не должны зависеть от поставщика решений (низкая связанность)

  5. Каждое разрабатываемое приложение должно иметь четкую, единообразную структуру (но бывает много исключений)

Комментарии (4)


  1. Tishka17
    02.11.2021 18:45

    Насколько я понял из статьи, хотя это не было выделено явно, FOR == filter+options+response. Вопросы:

    • К чему относятся права пользователя? Где здесь лежит авторизация?

    • Почему рассматривается только запрос поиска? Если пользователь создает продукт, куда лягут данные? в options?

    • Как быть когда представление данных отличается от того как они хранятся?

    • Как быть когда мы поверх одних данных и логики делаем как веб страницы, так и некоторое API, (например, для мобильных устройств)?

    Общую мысль я не вполне уловил, речь идет об описании API сервера (вспомнился GraphQL) или об архитектуре самого сервиса?


    1. TFStudio Автор
      02.11.2021 18:57

      Это концепция поиска данных, авторизация лежит выше.

      примерно: route -> controller -> DataProvider -> models -> DB

      на фронте вместо того чтобы сразу идти на бек (axios.get) - мы также реализуем поэтапно: DP->apiClass->Back Api

      мне всегда не хватало стандартных моделей, потому мы пришли к этому.

      Как быть когда представление данных отличается от того как они хранятся?

      -> у нас мы в Response указываем те хендлеры , которые надо применить к ответу, все DataHandlers - чистое функциональное программирование. Есть схема входящих данных, есть схема выходящих

      Почему рассматривается только запрос поиска?

      Эта часть цикла - только про поиск. Сохранение будет рассмотрено дальше, это тоже большой пласт подходов

      "Как быть когда мы поверх одних данных и логики делаем как веб страницы, так и некоторое API, (например, для мобильных устройств)?"

      а это я не понял


  1. dopusteam
    03.11.2021 07:08

    Data Provider имеет 2 механизма работы:

    Что за DataProvider? Вы сразу начинаете описывать механизмы, но не рассказали, что это

    Также мы тут добавляем некий массив примитивных операций со статичным интерфейсом, когда фильтры избыточны, их может быть достаточно много и они также могут скрывать внутри себя фильтры, обеспечивая единообразие и одновременно упрощенный синтаксис)

    Выглядит так, будто это разные уровни. Условно, примитивные операции являются более высокоуровневыми по сравнению с 'универсальным' методом поиска и должны бы вынесены в отдельный сервис, который уже сформирует нужный фильтр

    Options - определяет область поиска (все что не filter и не response)

    Непонятно от слова совсем. И примеров нет, что может быть в options

    Есть ли возможность объединить фильтры через ИЛИ?

    Кстати, почему методы статичные?

    вы в любой момент без изменения бизнес логики сможете отказаться от реализации (которая в примере) и заменить её на более оптимизированный код (или даже отдельный сервис)

    Непонятно, как я смогу изменить что то, если у меня на статику завязка)


    1. TFStudio Автор
      03.11.2021 10:41

      Да, извиняюсь за неглубокое повествование и уход сразу в детали.

      Что за DataProvider? Вы сразу начинаете описывать механизмы, но не рассказали, что это

      DataProvider - это отдельный слой приложения. Он находится внутри. Его назначение - это получение всех данных. Внутри него вы уже реализуете кеширование, логирование и прочее. Разработчикам мы оставляем простой метод для получения данных.

      Как сказано выше у него 2 метода. Первый универсальный, второй - массив простых действий, чтобы не писать каждый раз конструкцию options = {...} (исключение для простоты и сокращения кода. Второй - всегда использует первый.

      Options - Непонятно от слова совсем

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

      Есть ли возможность объединить фильтры через ИЛИ?

      В концепции нет никаких препятствий, в реализации, что приложено такое пока не заложено

      Кстати, почему методы статичные?

      Исключительно внутреннее соглашение, для единообразия и некое сокращения количества кода

      Непонятно, как я смогу изменить что то, если у меня на статику завязка)

      Рассматриваем модульное (микросервисное приложение) на php:

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

      можно переписать все на go - но это будет жутко дорого. вместо этого вы можете переписать внутренности DP на более высоко оптимизированном языке или любым иным способом

      Второй кейс: у вас react/vue.

      Есть некий компонент (форма создания какой-то элемента)

      Поля формы сильно отличаются от контекста и типа этого элемента (к примеры товары в магазине)

      Программист в начале имел всего 3 типа и всё было хорошо. Он за каждым ходит на бек, прямо внутри всего чудного компонента

      В данном кейсе - нам надо будет переписать и компонент тоже, когда мы захотим его оптимизировать, к примеру, добавив некий TTL-кеш

      Если бы он сразу написал FromProvider.get({type:any}) - тогда мы бы оптимизировали 1 участок (ядро системы), а не сотни вызовов axios.get()