В последние лет 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>
Данной технике будет, как говорится, "сто лет в обед", но свою нишу она имеет.

Итого

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

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


  1. AlexTest
    06.02.2016 21:00
    +2

    Решение(ли) какой именно проблемы описано(ли) в этой статье?


  1. CrackedSapphire
    06.02.2016 21:45
    +3

    Уже смешались люди, кони, Мечи, секиры, топоры


  1. lair
    06.02.2016 22:02
    +2

    Какую задачу вы пытаетесь решить (или уже решили)?


  1. artemonster
    06.02.2016 22:40
    +3

    что я прочитал?


  1. flancer
    07.02.2016 09:54
    -1

    Люблю я хабровчан за их неравнодушие! Этим-то и ценно общение — обменом информацией или эмоциями, на худой конец. В общем, никто не уйдет обиженным.

    Для себя я решил задачу экстрагирования в статье небольшого размера сути подхода по отделению обрабатываемых данных от обработчиков, что, на мой взгляд, является одним из краеугольных камней при распределенной разработке (слабосвязанными командами разработчиков) гетерогенных программных систем (типичный пример — web-приложение), интегрированных с внешними сервисами посредством SOAP/REST и адаптируемых конечным пользователем под изменяющиеся условия применения.

    Да, это довольно узкоспециализированное направление, достаточно сильно отличающееся, например, от «программирования игр под андроид», но, к сожалению, на habr'е нет отдельной категории для подобных направлений, поэтому я и запостился в общий хаб «Программирование». Прошу прощения у всех, кого отвлек этим своим постом от забот насущных.


    1. AlexTest
      07.02.2016 11:38

      1. Не думаю что уместно сравнивать применение аксессоров с «железной» архитектурой Эйкена.
      2. Применение аксессоров далеко не бесспорно. Судя по вашей дате рождения из профиля, вы (как и я) начинали с процедурного программирования и до сих пор подсознательно «тоскуете» по тем временам. Универсального контейнера данных не существует как и «серебряной пули» т.к. за любую универсальность приходится платить. Например в вашей терминологии:

      Самое главное, что универсальный контейнер каждой «вселенной» может разбирать и генерировать транскод (JSON/XML/YAML/...), адаптируя к своей среде выполнения не только те данные, которые заложил в него разработчик самого приложения («A»), но и дополнительные данные, которые прицепили к «посылке» разработчики сервиса («C») или клиента («B»).
      Вас не напрягает, что сервис («C») или клиент («B») могут не только добавлять свои данные, но и изменять данные приложения («A»), причем так, что приложение («A») даже не будет догадываться, что его данные кто-то поменял?


      1. flancer
        07.02.2016 18:30

        1. Я сравнивал с архитектурой Эйкена не акцессоры, а именно сам подход, когда данные отделяются от обработчиков.
        2. Любая DBMS является довольно-таки универсальным контейнером данных.
        3. Я использую подход с акцессорами только лишь из-за привычки ожидать подсказки от IDE после набора префикса get/set. Мне так удобнее. Чтобы не путать с другими методами при использовании автодополнения. Но если мы работаем с контейнером данных, то там не должно быть других методов, кроме акцессоров, так что можно обойтись и самими свойствами.
        4. В моем примере A, B и C — это представители различных уровней приложения, разных «вселенных». Это как запросить данные из программы на JavaScript у сервиса, написанного на Java. Java-сервис может изменять запрос сколько угодно — он работает с копией, созданной на основании транскода (XML в случае с SOAP).
        5. Менять входные данные — не очень хорошая практика, даже если ты создаешь весь код сам, а уж если ты работаешь в команде, то надо много раз подумать, чтобы решится на это.
        6. Если все-таки приходится сталкиваться с тем, что моя или чья-то еще функция/метод изменяет входные данные, то — да, меня это напрягает.


    1. michael_vostrikov
      07.02.2016 12:20

      Это не узкоспециализированное направление. Вы описали массив в PHP. Ваш DataObject практически ничем от него не отличается.

      Что имеем в результате? Контейнер для переноса любых данных.

      Массив в PHP это и есть контейнер для переноса любых данных, мы его имели и до результата.

      Можно все то же самое делать через аннотацию @property

      Почему бы просто не объявить нормальный класс с нормальными свойствами, унаследованный от ArrayObject? Потому что вам удобнее по-другому? Хороший аргумент, что сказать.

      Скрытый текст
      class GetForDependentCalc extends DataObject
      {
          public $baseCalcData;
          public $dependentCalcData;
      }
      
      
      $a = new GetForDependentCalc();
      $a->test = 'test';
      var_dump($a);
      
      /*
      object(GetForDependentCalc)[1]
        public 'baseCalcData' => null
        public 'dependentCalcData' => null
        public 'test' => string 'test' (length=4)
      */
      
      
      class DataObject extends ArrayObject
      {
          public function __construct($input = [], $flags = ArrayObject::STD_PROP_LIST, $iterator_class = 'ArrayIterator')
          {
              parent::__construct($input, $flags, $iterator_class);
          }
      }
      


      1. flancer
        07.02.2016 18:51

        1. Суть подхода вы уловили — в качестве контейнера можно использовать и массив, и ArrayObject. Самое главное, чтобы функционал, который реализован в контейнере, не зависел от типа данных, которые в нем содержатся. Как в том же ArrayObject.
        2. Можно при сериализации использовать и внешнюю функцию — это не принципиально. Принципиально, что сериализуются любые данные — и те, которые уложил в контейнер разработчик основного функционала, и те, которые в этот же контейнер уложил разработчики плагинов для основного функционала.
        3. Один входной объект для функции и один выходной — это принципиальный вопрос для конвейеризации. Если вы посмотрите на web-сервисы, то увидите, что именно так и есть — на вход подается одна структура данных (request), на выходе получается другая, но тоже одна (response). В request'е объединены воедино все данные, необходимые для выполнения операции сервисом (как и в запросе) — сервис сам может выбрать из запроса нужные ему данные (а плагин к сервису может выбрать из этого же запроса нужные ему).


    1. lair
      07.02.2016 13:51

      Для себя я решил задачу экстрагирования в статье небольшого размера

      Ну то есть вся задача статьи — это написать статью?

      сути подхода по отделению обрабатываемых данных от обработчиков,

      Извините, что? Разделение данных и логики? А вы про функциональное программирование вообще не слышали никогда?

      при распределенной разработке (слабосвязанными командами разработчиков) гетерогенных программных систем (типичный пример — web-приложение), интегрированных с внешними сервисами посредством SOAP/REST

      Паттерн Data Transfer Object — не? Buzzword SOA — не?

      Но самое главное — «универсальный контейнер данных» тут совсем ни при чем. Во всех описанных выше случаях используется вполне конкретный объект, предпочтительно — типизированный (если это позволяет язык и платформа), потому что иначе количество геморроя неописуемо.


      1. flancer
        07.02.2016 19:11

        1. Да. Причем попытаться уложиться в несколько экранов.
        2. Слышал.
        3. Если DTO не зависит от типа переносимых данных — да, это он и есть.
        4. Buzzword SOA — не понимаю, о чем это.
        5. Самое главное в этой концепции: мухи (код обработчиков) отдельно, а котлеты (обрабатываемые данные) — отдельно. А если «мухи» и/или «котлеты» будут в виде типизируемых объектов — так оно даже и лучше.


        1. ghost404
          07.02.2016 20:09

          Когда вы говорите «мухи», «котлеты», у меня создается ощущение что вы не слышали про MVC.
          По поводу вашего DataObject. Я подобное создавал в первый год изучения прогркммирования и очень бытсро отказался от этого решения. Основная проблема это отсутствие типизации и жесткой структуры объектов и с автодополнением в те годы были проблемы.


          1. flancer
            08.02.2016 07:44

            Если отталкиваться от аббревиатуры MVC, то M — это «чистые данные», а V & C — это типизация, наследование и все прочее. В «коде» (VC) можно и нужно использовать все, что позволяет уменьшить сложность и увеличить управляемость, а в данных (M) — только данные (аналог POJO). Потому что именно это уменьшает сложность и увеличивает управляемость. Код в данных — это как SQL-процедуры в БД.


            1. ghost404
              09.02.2016 07:54

              Так я и думал. Модель это не только данные, но еще и бизнес логика. Бизнес логика должна находится как можно ближе к данным. Именно по этому (еще из-за производительности) некоторые размещают бизнес логику в тригерах и процедурах БД, рядом с хранимыми данными. И именно по этому класс модели должен быть типизирован и хранить данные и бизнес логику одной конкретной сущности. Смотрите тот же ActiveRecord.

              Если же нужен контейнер для временного хранения данных, то, как и говорили michael_vostrikov и lair, достаточно обычного массива


              1. flancer
                09.02.2016 08:37

                По поводу использования обработчиков (SQL-процедур/функций и триггеров) рядом с данными есть различные мнения. Нет универсального решения — «каждому решению присуще сожаление» (с) Применяемый способ решения зависит от условий задачи. Обычный массив можно использовать в качестве «универсального контейнера», а можно для тех же целей его доработать (как это было сделано в той же Magento) и использовать его более эффективно. Сравните:

                    $transId = $data['Sales'][3]['Payments'][0]['Transactions'][0]['Id'];
                    $transId = $data['/Sales/3/Payments/0/Transactions/0/Id'];
                

                Во втором случае адрес элемента в массиве данных напоминает привычные пути (файловые, XPath, JSONPath), что позволяет использовать уже существующие методы работы с подобной информацией. При помощи аннотаций его можно слегка «дотипизировть», оставляя тем не менее дополняемым на уровне исполнения.

                Вы правы в том, что все данные одной не очень большой базы можно воткнуть в обычный ассоциативный массив. И да, это будет универсальный контейнер. А если его использовать без добавления инструкций по обработке, то это и будет универсальный контейнер данных. Вот только не будет отражена структура «известных данных», что изрядно усложнит разработку. Хотя можно сделать «типизатор» данных, который для любого произвольного массива будет извлекать данные нужной структуры. Это тоже путь.


                1. ghost404
                  09.02.2016 09:37

                  Вот только не будет отражена структура «известных данных», что изрядно усложнит разработку.


                  Поэтому нужно использовать классы которые будут описывать конкретную структуру «известных данных», а не ваш DataObject и массив.

                  Пример по вашем же данным

                  $transId = $data->getSales()[3]->getPayments()[0]->getTransactions()[0]->getId();
                  

                  И при правильном описании аннотации будет работать автодополнение на протчжении всей цепочки вызовов


                  1. flancer
                    09.02.2016 10:41

                    Ну вот! Так это то, о чем я и писал!!! Делая класс на базе DataObject вы делаете его:
                    а) типизируемым;
                    б) дополняемым;

                    Меня в Magento весьма сильно напрягало, что в таких структурах никогда точно не знаешь, что лежит, и сильно радовало, что в них всегда можно положить все, что угодно. Проблема в том, что в некоторых случаях множественное наследование не работает. Например, когда два-три расширения переопределяют один и тот же класс основного функционала. Разработчик каждого плагина не знает о существовании других плагинов, да и не должен. А я, как интегратор, должен сам решать в каком порядке мне выстроить иерархию наследования в конечном приложении. И иногда это бывает довольно забавной задачей, если учесть, что порой приходится совмещать в одном приложении по 15-20 сторонних плагинов.

                    В подобной ситуации вот такой DataObject + «гарвардский» подход (отделение данных от инструкций, функциональное программирование, если кому удобнее) может дать весьма ощутимые бонусы в виде конвейеризации обработчиков некоей «структуры данных».


                    1. ghost404
                      09.02.2016 12:18

                      Ну вот! Так это то, о чем я и писал!!! Делая класс на базе DataObject вы делаете его:
                      а) типизируемым;
                      б) дополняемым;

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

                      Проблема в том, что в некоторых случаях множественное наследование не работает. Например, когда два-три расширения переопределяют один и тот же класс основного функционала.

                      Множественное наследование и не должно работать. В вашем случае нужно использовать Адаптор. Использование DataObject в описанном вами случае больше похоже на костыль


                      1. flancer
                        09.02.2016 14:50

                        Смотрите, в базовой имплементации есть объект Customer, есть два плагина, разработчики которых не знают друг о друге, но знают, что есть базовая имплементация Customer'а. Каждый из разработчиков добавляет по одному атрибуту к базовой сущности Customer:

                        CREATE TABLE Customer (
                          Id int NOT NULL AUTO_INCREMENT COMMENT 'this is attribute from base implementation',
                          Ref varchar(255) DEFAULT NULL COMMENT 'this attribute is added by plugin 1',
                          Email varchar(50) DEFAULT NULL COMMENT 'this attribute is added by plugin 2',
                          PRIMARY KEY (Id)
                        )
                        

                        и создает свои собственные расширения базового объекта CustomerRef & CustomerEmail:
                        /**
                         * This is base object.
                         *
                         * @method int getId()
                         * @method void setId(int $data)
                         */
                        class Customer extends DataObject {
                        }
                        
                        /**
                         * This is extended customer (plugin 1).
                         *
                         * @method string getRef()
                         * @method void setRef(string $data)
                         */
                        class CustomerRef extends Customer {
                        }
                        
                        /**
                         * This is extended customer (plugin 2).
                         *
                         * @method string getEmail()
                         * @method void setEmail(string $data)
                         */
                        class CustomerEmail extends Customer {
                        }
                        

                        допустим, есть внешний класс для выполнения операций с БД (разделение инструкций и данных, детали его реализации на данный момент не важны). В этом случае в базовой имплементации подгружается объект со всеми своими атрибутами и используется в таком виде в базовом workflow. В местах, где включаются обработчики плагинов (допустим, по событию), они преобразовывают базовые данные в понятный для себя вид и работают со «знакомыми» атрибутами, игноря атрибуты незнакомые. В конце базового workflow происходит сохранение объекта в БД:
                                // data loader (base impl.)
                                $base = $repo->load('Customer', 21);
                                
                                // plugin1 code on event 1
                                $cust1 = new CustomerRef($base);
                                $id1 = $cust1->getId();
                                $ref = $cust1->getRef();
                        
                                // plugin2 code on event 2
                                $cust2 = new CustomerEmail($base);
                                $id2 = $cust2->getId();
                                $cust2->setEmail('any@email.com');
                        
                                // data saver (base impl.)
                                $repo->save('Customer', $cust2);
                        

                        По сути дела производный от DataObject класс в некотором роде и является для среды выполнения адаптером к данным, хранимым в ассоциативном массиве. Все то же самое, при желании, можно изобразить и просто на ассоциативном массиве, только без autocomplete'а в IDE и без возможности поиска Find Usages.


                        1. ghost404
                          09.02.2016 18:52
                          +1

                          Вот. С этого и стоило начинать. Это уже нормальное описание проблемы. Его и нужно было приводить в статье. Тогда и вопросов было бы меньше.

                          Пример решения с использованием декораторов и включенным strict mode:

                          Структура классов
                          declare(strict_types=1);
                          
                          interface CustomerInterface
                          {
                              public function getId(): int;
                              public function setId(int $id);
                          }
                          
                          interface CustomerPluginInterface extends CustomerInterface
                          {
                              public function getCustomer();
                          }
                          
                          // This is base object.
                          class Customer implements CustomerInterface
                          {
                              private $id;
                              private $ref;
                              private $email;
                          
                              public function getId(): int
                              {
                                  return $this->id;
                              }
                          
                              public function setId(int $id)
                              {
                                  $this->id = $id;
                                  return $this;
                              }
                          
                              public function getRef(): string
                              {
                                  return $this->ref;
                              }
                          
                              public function setRef(string $ref)
                              {
                                  $this->ref = $ref;
                                  return $this;
                              }
                          
                              public function getEmail(): string
                              {
                                  return $this->email;
                              }
                          
                              public function setEmail(string $email)
                              {
                                  $this->email = $email;
                                  return $this;
                              }
                          }
                          
                          // This is extended customer (plugin 1).
                          class CustomerRef implements CustomerPluginInterface
                          {
                              private $customer;
                          
                              public function __construct(Customer $customer)
                              {
                                  $this->customer = $customer;
                              }
                          
                              public function getId(): int
                              {
                                  return $this->customer->getId();
                              }
                          
                              public function setId(int $id)
                              {
                                  $this->customer->setId($id);
                                  return $this;
                              }
                          
                              public function getRef(): string
                              {
                                  return $this->customer->getRef();
                              }
                          
                              public function setRef(string $ref)
                              {
                                  $this->customer->setRef($ref);
                                  return $this;
                              }
                          
                              public function getCustomer()
                              {
                                  return $this->customer;
                              }
                          }
                          
                          // This is extended customer (plugin 2).
                          class CustomerEmail implements CustomerPluginInterface
                          {
                              private $customer;
                          
                              public function __construct(Customer $customer)
                              {
                                  $this->customer = $customer;
                              }
                          
                              public function getId(): int
                              {
                                  return $this->customer->getId();
                              }
                          
                              public function setId(int $id)
                              {
                                  $this->customer->setId($id);
                                  return $this;
                              }
                          
                              public function getEmail(): string
                              {
                                  return $this->customer->getEmail();
                              }
                          
                              public function setEmail(string $email)
                              {
                                  $this->customer->setEmail($email);
                                  return $this;
                              }
                          
                              public function getCustomer()
                              {
                                  return $this->customer;
                              }
                          }
                          

                          под рукой нет php7 чтобы проверить, но должно работать


        1. lair
          07.02.2016 20:18

          Да. Причем попытаться уложиться в несколько экранов.

          То есть вы считаете, что статья — самоценна?

          Слышал

          Тогда чем суть вашего подхода отличается от функциональной парадигмы?

          Если DTO не зависит от типа переносимых данных — да, это он и есть.

          DTO бывает разный. DTO, как паттерн, не заивисит от типа переносимых данных.

          SOA — не понимаю, о чем это.

          Вы не знаете, что такое service-oriented architecture?

          мухи (код обработчиков) отдельно, а котлеты (обрабатываемые данные) — отдельно.

          Повторюсь еще раз: зачем для этого универсальный «контейнер данных»?

          А если «мухи» и/или «котлеты» будут в виде типизируемых объектов — так оно даже и лучше.

          … а тогда никакого универсального контейнера тоже не будет.


          1. flancer
            08.02.2016 07:55

            1. Да. Теперь мои мысли приобрели форму ссылки. Это удобно.
            2. В некотором смысле этот подход является следствием функциональной парадигмы.
            3. В таком случае «DTO — как паттерн» можно использовать в качестве универсального контейнера данных;
            4. Знаю.
            5. Чтобы не смешивать данные и обрабатывающий их код — как в DTO.
            6. Если типизируемый объект позволит добавлять к себе любую структуру данных и выдавать ее — то он и будет являться универсальным контейнером. Именно это и позволяют делать объекты, наследующие от DataObject.


            1. lair
              08.02.2016 10:44

              Да. Теперь мои мысли приобрели форму ссылки. Это удобно.

              Когда программист использует — или придумывает — какой-то подход, он делает это, чтобы решить конкретную задачу (или группу задач). Какую задачу решали вы?

              В некотором смысле этот подход является следствием функциональной парадигмы.

              Чем он от нее отличается?

              В таком случае «DTO — как паттерн» можно использовать в качестве универсального контейнера данных;

              Нет, нельзя. DTO — это конкретный паттерн, используемый в конкретных случаях.

              Знаю

              Тогда чем предлагаемое вами решение «для гетерогенного приложения» отличается от SOA?

              Чтобы не смешивать данные и обрабатывающий их код — как в DTO.

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

              Если типизируемый объект позволит добавлять к себе любую структуру данных и выдавать ее — то он и будет являться универсальным контейнером.

              В этот момент он перестал быть типизируемым — потому что «любая структура данных» противоречит типизации (статической, понятное дело).

              Повторюсь еще раз: чем предлагаемый вами подход «по отделению обрабатываемых данных от обработчиков» отличается от обычного функционального программирования?


              1. flancer
                08.02.2016 13:59

                1. Я решал задачу уменьшения сложности при производстве ПО.
                2. Функциональная парадигма делает акцент на вычислениях, я же делаю ацент на данных. В обоих случаях рассматривается один и тот же посыл — «отделение данных от инструкций», который был предложен еще 70-80 лет назад Говардом Эйкеном.
                3. В таком случае DTO нельзя использовать как «универсальный контейнер данных».
                4. Не отличается, но дополняет — вводит расширяемость на уровне данных в «чётко определённые интерфейсы» SOA.
                5. «Универсальный контейнер данных» — это более узкий термин, чем «любой способ обработки структурированных данных». «Контейнер» подразумевает краткосрочное хранение при передаче, «универсальный» подразумевает независимость от содержимого, «данные» подразумевают данные. Это все равно спросить «Зачем вводить понятие 'функционального программирования', когда уже есть всем понятный термин 'программирование'».
                6. В таком случае, типизируемый объект не может являться универсальным контейнером данных.
                7. См. п.2


                1. lair
                  08.02.2016 14:05

                  Я решал задачу уменьшения сложности при производстве ПО.

                  Каким образом ваше решение уменьшает сложность?

                  Функциональная парадигма делает акцент на вычислениях, я же делаю ацент на данных.

                  На основании чего вы делаете такое утверждение о функциональной парадигме? В чем конкретно выражается ваш «акцент на данных»?

                  В таком случае DTO нельзя использовать как «универсальный контейнер данных».

                  Но при этом DTO полностью удовлетворяет задаче «по отделению обрабатываемых данных от обработчиков, что, на мой взгляд, является одним из краеугольных камней при распределенной разработке (слабосвязанными командами разработчиков) гетерогенных программных систем (типичный пример — web-приложение), интегрированных с внешними сервисами посредством SOAP/REST и адаптируемых конечным пользователем под изменяющиеся условия применения.»

                  Не отличается, но дополняет — вводит расширяемость на уровне данных в «чётко определённые интерфейсы» SOA.

                  А почему вы думаете, что в SOA (а) четко определенные интерфейсы и (б) нет расширяемости на уровне данных? Вы знаете, как расшифровывается XML?

                  «Универсальный контейнер данных» — это более узкий термин, чем «любой способ обработки структурированных данных». [...] Это все равно спросить «Зачем вводить понятие 'функционального программирования', когда уже есть всем понятный термин 'программирование'».

                  Вы не поняли. Речь не о вводе термина, а о том, что для решения задачи достаточно любого способа структурирования данных. Какие преимущества появляются именно от вашего «универсального контейнера»?

                  В таком случае, типизируемый объект не может являться универсальным контейнером данных.

                  Именно. Но при этом полностью удовлетворяет описанной задачи.


                  1. flancer
                    08.02.2016 20:51

                    1. А разве это не очевидно?

                    /**
                     * @method array getBaseCalcData()
                     * @method void setBaseCalcData(array $data)
                     */
                    

                    вместо
                        private $_baseCalcData;
                        public function getBaseCalcData() {
                            return $this->_baseCalcData;
                        }
                        public function setBaseCalcData(array $data) {
                            $this->_baseCalcData = $data;
                        }
                    

                    Даже визуально видно, что сложность уменьшилась минимум в 2 раза.

                    2. На основании названия.

                    3. Если DTO полностью удовлетворяет условиям, значит его можно использовать в качестве «универсального контейнера данных».

                    4. Если есть расширяемость на уровне данных, то значит в SOA уже используется концепция «универсального контейнера данных». Да, я знаю, как расшифровывается XML.

                    5. Нет, это вы не поняли. Если вы используете любой способ структурирования данных для формирования «посылки» с произвольным содержимым, то вы таким образом создаете «универсальный контейнер данных». Даже если сами об это не подозреваете. А я просто обращаю ваше внимание на этот аспект «обработки структурированных данных». Вы могли всю жизнь программировать в определенном стиле, не подозревая об этом, а потом я пришел и сообщил, что для вашего стиля характерны определенные черты, и дал ему название «функциональное программирование».

                    6. Если он полностью удовлетворяет, то является.


                    1. lair
                      08.02.2016 22:46

                      А разве это не очевидно?

                      Нет, не очевидно. Сравните оба ваших варианта с:
                      object[] BaseCalcData {get; set;}
                      


                      И, заметим, никакого универсального контейнера (object там только потому, что я не знаю, какой именно у вас тип под массивом).

                      На основании названия.

                      Название обманчиво. LISP, весь из себя насквозь функциональный, чуть более чем полностью состоит из структур данных.

                      Если DTO полностью удовлетворяет условиям, значит его можно использовать в качестве «универсального контейнера данных».

                      Нет. Напомню задачу: «отделение обрабатываемых данных от обработчиков, что [...] является одним из краеугольных камней при распределенной разработке [...] гетерогенных программных систем [...], интегрированных с внешними сервисами посредством SOAP/REST и адаптируемых конечным пользователем под изменяющиеся условия применения». В этой задаче прекрасно используется строго типизованный DTO.

                      Если есть расширяемость на уровне данных, то значит в SOA уже используется концепция «универсального контейнера данных».

                      В SOA может использоваться «универсальный контейнер». Намного чаще там используется практика «игнорирую то, чего не знаю».

                      Если вы используете любой способ структурирования данных для формирования «посылки» с произвольным содержимым, то вы таким образом создаете «универсальный контейнер данных».

                      Нет. Когда я сериализую число в бинарный поток, я формирую сообщение с произвольным содержимым, но я не создаю никакого универсального контейнера. Более того, даже когда я использую ассоциативный массив — я использую именно ассоциативный массив, и то, что вам хочется назвать его «универсальным контейнером», никак не отменяет того, что это просто ассоциативный массив.


                      1. flancer
                        09.02.2016 10:53

                        1. Сравнил. Это точно не PHP.
                        2. Я имел в виду не LISP, я имел в виду именно «функциональное программирование».
                        3. «Строго типизированный» DTO не удовлетворяет условию «распределенной разработки» — там по определению не может быть строгой типизации.
                        4. Практика «игнорирую то, чего не знаю» весьма хорошо сочетается с «универсальным контейнером».
                        5. То, что это ассоциативный массив никак не отменяет того, что он может использоваться в качестве «универсального контейнера данных».


                        1. lair
                          09.02.2016 11:06

                          Сравнил. Это точно не PHP.

                          А какая разница? Или ваш подход применим только и исключительно к PHP?

                          Я имел в виду не LISP, я имел в виду именно «функциональное программирование».

                          Покажите мне функциональный язык (из мейнстрима), в котором нет обширной поддержки структур данных.

                          «Строго типизированный» DTO не удовлетворяет условию «распределенной разработки» — там по определению не может быть строгой типизации.

                          Это почему это не может? Если все стороны договорились о четкой структуре сообщений, которыми они обмениваются, получается строгая типизация.

                          Практика «игнорирую то, чего не знаю» весьма хорошо сочетается с «универсальным контейнером».

                          Это, однако, не означает, что в SOA используется «универсальный контейнер».

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

                          Может, но какой от этого выигрыш?


    1. babylon
      07.02.2016 21:35
      -1

      Я бы вам поднял карму, но я сам в минусе. Посмотрите в сторону JSONNET.


      1. flancer
        08.02.2016 08:00

        Спасибо на добром слове. Посмотрел. JSONNET — это именно то, о чем я написал (разделение кода и данных), вывернутое наизнанку (совмещение данных и их обработки).