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

от автора

В последние лет 5 я, по большей части, имею дело с приложениями на базе Magento, в основу которой заложены идеи максимальной гибкости, расширяемости и адаптивности. Популярность Magento в e-commerce и количество сторонних модулей расширений к ней говорят о том, что эта платформа и реализованные в ней идеи скорее успешные, чем наоборот. В основу большого количества объектов в Magento заложена концепция универсального контейнера данных (Varien_Object в Magento 1 и \Magento\Framework\DataObject в 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>

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

Итого

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

ссылка на оригинал статьи https://habrahabr.ru/post/273261/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *