Универсальный контейнер данных

в 13:23, , рубрики: данные, Программирование

В последние лет 5 я, по большей части, имею дело с приложениями на базе Magento, в основу которой заложены идеи максимальной гибкости, расширяемости и адаптивности. Популярность Magento в e-commerce и количество сторонних модулей расширений к ней говорят о том, что эта платформа и реализованные в ней идеи скорее успешные, чем наоборот. В основу большого количества объектов в Magento заложена концепция универсального контейнера данных (Varien_Object в Magento 1 и MagentoFrameworkDataObject в Magento 2). Я нахожу у подобных универсальных контейнеров определенные параллели с такими явлениями, как

  • POJO (Java)
  • JSON
  • XPath
  • DOM
  • СУБД (реляционные и не очень)
  • SPA (Single Page Applications)

ну и в конце концов — с Гарвардской архитектурой ЭВМ, разработанной Говардом Эйкеном в 1930-х годах.

Что такое "универсальный контейнер данных"?

Это обычный ассоциативный массив, карта (map) в котором каждому ключу соответствуют какие-то данные, включая другие ассоциативные массивы. Контейнер содержит, таким образом, дерево данных с одной точкой входа и отсутствием замкнутых контуров (весьма желательное условие). Что-то типа:

        $customer = ['Id' => 21, 'Name' => 'Janis Zarinsh'];
        $order = ['Id' => 32, 'Amount' => 43.32, 'Currency' => 'EUR'];
        $data = [];
        $data['Customer'] = $customer;
        $data['SaleOrder'] = $order;

Любой "лист" дерева или часть дерева (поддерево) адресуется "путем" — перечислением всех ключей по дороге к цели:

        $orderId = $data['SaleOrder']['Id'];

В PHP с использованием magic-методов можно реализовать то же самое в таком виде:

        $customer = new DataObject(['Id' => 21, 'Name' => 'Janis Zarinsh']);
        $order = new DataObject(['Id' => 32, 'Amount' => 43.32]);
        $order->setCurrency('EUR');
        $data = new DataObject();
        $data->setCustomer($customer);
        $data->setSaleOrder($order);

Адресация в более привычном виде (как путь к файлу в *nix):

        $orderId = $data->getData('/SaleOrder/Id');

Что имеем в результате? Контейнер для переноса любых данных. Описание PHP разработчиком объектов, аналогичных POJO в Java, сводится к аннотированию их акцессоров (get/set методов) для того, чтобы можно было использовать автодополнение в IDE. Можно все то же самое делать через аннотацию @property, это будет даже несколько короче (правда потребует другой реализации DataObject, через get, set), но мне удобнее вот так:

/**
 * @method array getBaseCalcData()
 * @method void setBaseCalcData(array $data)
 * @method array getDependentCalcData()
 * @method void setDependentCalcData(array $data)
 */
class GetForDependentCalc extends DataObject {}

Как автор этого объекта я определил те свойства, которые я использую в своих целях. Универсальный контейнер передаст используемые мной данные от одного обработчика данных к другому (например, от одного моего сервиса с другому моему сервису) и совершенно не будет против, если в процессе транспортировки в него будут добавлены другие данные (например, каким-либо расширением, написанным совершенно другим разработчиком). Более того, можно "научить" универсальный контейнер автоматически преобразовывать хранимые в нем данные в формат, например, JSON и передать эти данные с серверной стороны в браузер. И вместе с моими данными контейнер также преобразует и данные, подготовленные сторонним расширением моего кода на серверной стороне и используемые сторонним расширением моего кода на стороне клиента.Некоторым образом универсальный контейнер данных противоречит объектно-ориентированной парадигме, разделяя в приложении чистые объекты-данные и объекты-обработчики. Но это, скорее даже, не противоречие, а граничный случай использования ООП — как POJO. Универсальный контейнер данных так же хорошо может сосуществовать с ООП, как и RDBMS — ведь, в конце-концов, RDBMS — это тоже своего рода "универсальный контейнер данных". Теоретически, любую базу данных можно поместить в ассоциативный массив (если мы говорим именно про данные, а не про обработчики — триггера/процедуры/функции).

"Зачем нам весь этот тюнинг в зоопарке?"

Расширяемость

Magento, помимо своего основного предназначения в виде платформы для создания интернет-магазинов, также является средой для создания расширений своего базового функционала. Существует великое множество плагинов к Magento — простых и сложных, бесплатных и коммерческих (некоторые из которых весьма недешевы). И универсальный контейнер данных является базовым концептом в ее архитектуре. Правда в Magento он в основном используется как ядро для построения большинства остальных компонентов системы (родительский класс), а не является "чисто данными". Тем не менее своей расширяемостью Magento не в последнюю обязана именно ему. Подобный подход может быть полезен в любых платформах, которые подразумевают открытость к созданию для них расширений сторонними разработчиками.

"Дальний космос"

Что объединяет любые приложения, так это то, что они все обрабатывают данные. Как правило, данные сохраняются в базе, извлекаются из нее и помещаются обратно слоем бизнес-логики, трансформируются для представления в удобном для пользователя виде на уровне UI, и там же получаются от пользователя и трансформируются в удобный для обработки и последующего хранения вид. Иногда, а в web-приложениях практически всегда, данные передаются из одной "вселенной" (серверный слой бизнес-логики, например, на PHP) в другую "вселенную" (клиентский презентационный слой на JavaScript). И в это путешествие, как правило, отправляются только данные — в виде JSON/XML/… Весь функционал остается на месте, он попросту не применим в "другой вселенной".Универсальный контейнер данных "вселенной A" (PHP) может преобразовать свои данные в транскод (например, JSON) и отправить их во "вселенную B" (JavaScript), или преобразовать в другой транскод (например, XML) и отправить данные во "вселенную C" (например, SOAP-сервис на Java). Или "B" сначала может преобразовать и отправить данные в "C", затем получить ответ от "C", обработать и отправить в "A", а "A", при необходимости, может и сама обратиться в "C". Самое главное, что универсальный контейнер каждой "вселенной" может разбирать и генерировать транскод (JSON/XML/YAML/...), адаптируя к своей среде выполнения не только те данные, которые заложил в него разработчик самого приложения ("A"), но и дополнительные данные, которые прицепили к "посылке" разработчики сервиса ("C") или клиента ("B").

Гибкая конвейеризация

Функции допускают задание множества аргументов, но результат, как правило, возвращается единственен:

function tooManyArgs($arg1, $arg2, $arg3) {}

Если ограничить количество входных аргументов в функцию-процессор одним единственным аргументом (как и результат работы функции):

function oneArgOnly($data) {}

то можно получить весьма интересные последствия в виде цепочек функций-процессоров, где выходные данные одних функций являются входными данными для других. Пример практического применения подобного подхода — обещания в JavaScript:

httpGet(...)
  .then(...)
  .then(...)
  .then(...)

В PHP конвейер обработчиков мог бы выглядеть примерно так:

function proc5(DataObject $arg)
{
    $result = new DataObject();
    $customer = $arg->getData('/Customer');
    $order = $arg->getData('/SaleOrder');
    // ...
    $result->setData('/Transaction', $trans);
    return $result;
}

function proc6(DataObject $arg)
{
    $result = new DataObject();
    $transaction = $arg->getData('/Transaction');
    // ...
    $result->setData('/Balance', $balance);
    return $result;
}

$res5 = proc5($data);
$res6 = proc6($res1);

$amount = $res6->getData('/Balance/Amount');

Можно из набора подобных функций-процессоров на описательном уровне строить поток обработки данных:

<workflow>
    <step id="5">
        <handler id="proc5">
            <input>
                <map as="/Customer">
                    <handler>proc3</handler>
                    <result/>
                </map>
                <map as="/SaleOrder">
                    <handler>proc4</handler>
                    <result/>
                </map>
            </input>
        </handler>
    </step>
    <step id="6">
        <handler id="proc6">
            <input>
                <map as="/Transaction">
                    <handler>proc5</handler>
                    <result/>
                </map>
            </input>
        </handler>
    </step>
</workflow>

и изменять его в зависимости от обрабатываемых данных:

<workflow>
    <step id="7">
        <case>
            <condition if="qt">
                <left output="proc5">/Balance/Amount</left>
                <right>0</right>
            </condition>
            <then>
                <handler id="proc7">...</handler>
            </then>
            <else>
                <handler id="proc8">...</handler>
            </else>
        </case>
    </step>
</workflow>

Данной технике будет, как говорится, "сто лет в обед", но свою нишу она имеет.

Итого

С моей точки зрения "Гарвардский подход" мистера Говарда Эйкена по разделению кода и данных может стать базой для достаточного количества интересных решений в области разработки ПО."Будем искать!" (с) С. С. Горбунков

Автор: flancer

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js