Обновить
8K+
51
Alex Gusev@flancer

Я кодирую, потому что я кодирую…

8,1
Рейтинг
99
Подписчики
Отправить сообщение

Когда-то давным-давно я пытался писАть программы, которые работают при любых условиях. Сейчас мне достаточно, чтобы они отрабатывали при определенных. А что касается любимых приемов, то "на цвет и вкус все фломастеры разные" (с)

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

Статья о рекурсии, а не о конвертации ассоциативного массива в объект. Я выложил пример рекурсии, а вы предложили сделать "по-другому". Я поинтересовался, можно ли сделать универсальную фабрику/строитель и получил ответ на свой вопрос. Если вас действительно интересует, как в _typePropsRegistry реализована обработка опциональных и обязательных полей и все остальное — то можете глянуть. Это не мой код, но я брал его за основу, т.к. этот не поддерживает объявление методов через аннотации.


ну и еще. рефлексия какбы довольно тяжелый механизм

Как бы да. Но на общем фоне обработки HTTP-запроса уже как бы и нет.

Спасибо за пример. Насколько я понял, итоговый код фабрик/строителей для 10-15 объектов выйдет за пределы одного экрана. Более того, с увеличением кол-ва объектов, используемых в API, кол-во фабрик/строителей (как следствие — кол-во кода) также будет расти. В моем случае парсер один и для одного объекта, и для сотни, как и регистратор _typePropsRegistry, который анализирует через рефлексию заданный тип и формирует массив доступных для инициализации свойств.

Можете считать, что объект не зависит от своих составляющих.

Как вам будет угодно считать. Вот пример агрегации из wiki:


class Ehe // Пример агрегации
{
private:
    Person& _partner1; // Enthaltener Teil.  // Aggregation
    Person& _partner2; // Enthaltener Teil.  // Aggregation

public:
    // Конструктор
    Ehe (Person& partner1, Person& partner2)
        : _partner1(partner1), _partner2(partner2)
    { }
};

если я не ошибаюсь, то агрегация — это зависимость, а _partner1 — это поле.

Да. сложные объекты (complex type) все имеют метод setData($property, $value). Но можно и по-другому, например setProperty($value). Перегоняется JSON, приходящий на API-сервис в объект, содержащий данные (аналог java beans). Был при признателен за пример универсальной фабрики или строителя для создания подобных объектов, код которого бы не выходил за рамки экрана.

Реку́рсия — определение, описание, изображение какого-либо объекта или процесса внутри самого этого объекта или процесса, то есть ситуация, когда объект является частью самого себя.


Это вопрос формулировок. Я считаю что объект зависит от частей, из которых состоит, вы — что нет.


Рекурсивные функции — их легко тестировать. Подаем что-то на вход и ожидаем что-то на выходе. Никаких моков не нужно для этого.

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


Вы настаиваете на том, что при тестировании рекурсивных функций мокирование категорически неприемлемо?

А можете точнее указать, где именно вы видите дублирование? Было бы хорошо, если бы вы представили свою версию метода, без лишнего дублирования. Это бы сразу придало вес вашим словам. Чтобы было понятнее, поясняю: на вход подается строка с названием типа объекта (класс) и ассоциативный массив данных, на выходе ожидается проинициализированный объект заданного типа. Свойства объекта (properties) могут быть простыми (строка, число), сложными (другой объект с иерархической структурой) или массивом простых или сложных объектов. Метод небольшой, укладывается с один экран, если убрать лишнее дублирование — получится еще меньше. Не думаю, что это займет у вас много времени.

Рекурсивная функция не может быть проще, чем она же. Поэтому мокая рекурсивный вызов вы не получаете упрощения

Для простых случаев, типа расчета факториалов, это справедливо, но если рекурсивная функция достаточно сложная, то получаю. Вот, например, преобразование ассоциативного массива в объект заданного типа:


    public function parseArrayData($type, $data)
    {
        $isArray = $this->_toolType->isArray($type);
        $typeNorm = $this->_toolType->normalizeType($type);
        $typeData = $this->_typePropsRegistry->register($typeNorm);
        if ($isArray) {
            /* process $data as array of $types */
            $result = [];
            foreach ($data as $key => $item) {
                $result[$key] = $this->parseArrayData($typeNorm, $item);
            }
        } else {
            /* process $data as data object of $type */
            $result = $this->_manObj->create($typeNorm);
            foreach ($data as $key => $value) {
                $propName = $this->_toolType->formatPropertyName($key);
                if (isset($typeData[$propName])) {
                    $propertyData = $typeData[$propName];
                    $propertyType = $propertyData->getType();
                    $propertyIsArray = $propertyData->getIsArray();
                    if ($propertyIsArray) {
                        /* property is the array of types */
                        $propertyType = $this->_toolType->getTypeAsArrayOfTypes($propertyType);
                        $complex = $this->parseArrayData($propertyType, $value);
                        $result->setData($propName, $complex);
                    } else {
                        if ($this->_toolType->isSimple($propertyType)) {
                            /* property is the simple type */
                            $result->setData($propName, $value);
                        } else {
                            /* property is the complex type, we need to convert recursively */
                            $complex = $this->parseArrayData($propertyType, $value);
                            $result->setData($propName, $complex);
                        }
                    }
                }
            }
        }
        return $result;
    }

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

Нет, я не мокаю циклы, хотя с удовольствием посмотрел бы на пример. Я мокаю именно зависимости. Рекурсивная функция становится зависимой от самой себя в тот момент, когда обращается сама к себе. Или я и зависимость неправильно понимаю?

Это как-то противоречит тому, что я написал?

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

Итерации, как правило, сложнее рекурсии в написании и восприятии. А создание сложных входных структур противоречит нами обоими одобренному тезису "Сложная логика в тестах тоже неверно." Но если вы можете сделать тестирование рекурсии простым и без моков — делайте простым и без моков.

Это может говорить о том что ваш рекурсивный метод возвращает результат разных типов? Это неверно.

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


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

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


function factorial($x)
{
    if ($x === 0) {
        return 1;
    } else {
        return $x * factorial($x - 1);
    }
}

Сложная логика в тестах тоже неверно.

Абсолютно согласен.

Кстати, можете еще попробовать вытащить данные (email & full name) по всем клиентам, которые совершили транзакции (sales_payment_transaction, нужны данные из поля tnx_id), которые соответствуют определенным методам платежа (sales_order_payment.method) по заказам, созданным в определенный промежуток времени, используя всю мощь Magento ORM.

Вы немножко неправильно поняли назначение моего кода — он подменяет алиасы для дополнительных столбцов исходного SQL'а для WHERE-правила (да-да, в background'е "Magento ORM" спрятан самый обычный SQL, впрочем, как и в background'е других ORM framework'ов) полным значением имени столбца, с добавлением алиаса таблицы.


В вашем примере код


$collection->addFieldToFilter('mytable.myfield',$yuorFilterValue)

просто добавляет в список WHERE-правил еще одно условие. Этот код и так исполняется, когда Magento разбирает условия фильтрации данных грида, заданные пользователем через WebUI (трассировка от \Magento\Framework\View\Element\UiComponent\DataProvider\FilterPool::applyFilters как раз и выведет на метод "addFieldToFilter"). У Magento-коллекции есть метод "addFilterToMap", который позволяет ввести карту преобразований, аналогичных тем, которые делаю я, и выполнять их перед тем, как добавить условие фильтрации (\Magento\Framework\Data\Collection\AbstractDb::_translateCondition, вызывается из addFieldToFilter), вот только нет возможности вклиниться в поток выполнения команд через событие (применение фильтров идет после создания коллекции и до генерации события "core_collection_abstract_load_before"). Можно использовать механизм плагинов и обернуть, например, метод \Magento\Framework\View\Element\UiComponent\DataProvider\CollectionFactory::getReport, чтобы он выполнял те же самые действия, что и в обсервере, плюс добавлял маппинг.


Регистрация around-плагина:
etc/di.xml


<type name="Magento\Framework\View\Element\UiComponent\DataProvider\CollectionFactory">
    <plugin
            name="vendor_module_data_provider_collection_factory"
            type="Vendor\Module\Plugin\Framework\View\Element\UiComponent\DataProvider\CollectionFactory"
            sortOrder="100"
            disabled="false"
    />
</type>

Код самого плагина, вызывающий класс-модификатор для добавления JOIN'ов к выборке и маппинг полей для их преобразования в фильтрах:


namespace Vendor\Module\Plugin\Framework\View\Element\UiComponent\DataProvider;
class CollectionFactory
{
    protected $_subQueryModifier;

    public function __construct(
        Sub\QueryModifier $subQueryModufier
    ) {
        $this->_subQueryModifier = $subQueryModufier;
    }

    public function aroundGetReport(
        \Magento\Framework\View\Element\UiComponent\DataProvider\CollectionFactory $subject,
        \Closure $proceed,
        $requestName
    ) {
        $result = $proceed($requestName);
        if ($requestName == 'customer_listing_data_source') {
            if ($result instanceof \Magento\Customer\Model\ResourceModel\Grid\Collection) {
                /* add JOINs to the select query */
                $this->_subQueryModifier->populateSelect($result);
                /* add fields to mapping */
                $this->_subQueryModifier->addFieldsMapping($result);
            }
        }
        return $result;
    }
}

Код, который модифицирует выбоку аналогичный тому, что в статье. Код для маппинга трививален:


    // depth
    $fieldAlias = self::AS_FLD_CUSTOMER_DEPTH;
    $fieldFullName = self::AS_TBL_CUST . '.' . Customer::ATTR_DEPTH;
    $collection->addFilterToMap($fieldAlias, $fieldFullName);

Этот подход позволяет использовать механизмы Magento для замены алиасов истинными именами полей вместо "грязного хака".

На это я уже ответил


Я хотел для примера воткнуть хоть какие-то более-менее правдоподобные зависимости в конструктор, чтобы не писать "ISomeService1", ..., "ISomeService4", а получилось, что я нарушил "принцип единственной ответственности".

Если заменить в моей статье


\Psr\Log\LoggerInterface $logger,
\Zend_Db_Adapter_Pdo_Abstract $dba,
ISomeService $service,

на


ISomeService1 $service1,
ISomeService2 $service2,
ISomeService3 $service3,

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


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

Какой статьи? "DI, PHPUnit и setUp"? Там есть хоть слово про декораторы или интерфейсы? Каким образом вы связали декораторы и изложенное в статье, по каким ключевым словам? По слову "тестирование"?

Информация

В рейтинге
948-й
Откуда
Рига, Латвия, Латвия
Дата рождения
Зарегистрирован
Активность

Специализация

Фулстек разработчик
Ведущий
От 3 000 €
JavaScript
HTML
CSS
Node.js
Vue.js
Веб-разработка
Progressive Web Apps
PostgreSQL
MySQL
GitHub