В последние лет 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