All streams
Search
Write a publication
Pull to refresh
49
-0.6
Alex Gusev @flancer

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

Send message

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


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


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

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


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

А можете точнее указать, где именно вы видите дублирование? Было бы хорошо, если бы вы представили свою версию метода, без лишнего дублирования. Это бы сразу придало вес вашим словам. Чтобы было понятнее, поясняю: на вход подается строка с названием типа объекта (класс) и ассоциативный массив данных, на выходе ожидается проинициализированный объект заданного типа. Свойства объекта (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"? Там есть хоть слово про декораторы или интерфейсы? Каким образом вы связали декораторы и изложенное в статье, по каким ключевым словам? По слову "тестирование"?

А чего вы к декораторам привязались? Статья про тестирование, а не про полиморфизм. Тестирование без конструирования невозможно, а без полиморфизма — вполне. Еще раз, с точки зрения создания объекта очень сильно незначительно важно, сколько у него параметров в конструкторе — 2 или 3.


Весь остальной наш флейм к сути вопроса, освещаемого в статье, имеет весьма малое отношение. Даже еще меньшее, чем имеет конструктор класса и его параметры к имплементируемым классом интерфейсам.

Возможно, я плохо выражаю свои мысли. Если вы посмотрите на мои примеры, то я нигде не указывал, что конструктор соответствует классу, имплементирующему какой-то определенный интерфейс. Интерфейс, который имплементируется, вообще остается за рамками вопроса, рассматриваемого в статье. А если не придумывать лишнего, то как я и сказал, разница между конструктором с 3-мя параметрами, и конструктором с 2-мя параметрами — несущественная.

ORM — штука которая представляет запросы к б.д. на объктно ориентированном диалекте.

Ну и как будет выглядеть запрос на выборку объектов с составным первичным ключом в Magento ORM? А на обновление объекта?

Я пересел на Magento с Java, на которой имел дело и с Hibernate, и с DataNucleus. То, что в статье говорится "It should be no surprise that Magento takes the ORM approach", не является основанием для заявления, что ORM в Magento присутствует. Для разминки изобразите средствами "Magento ORM" сущность с составным первичным ключом (состоящим из двух полей таблицы в БД), а затем попробуйте средствами "Magento ORM" извлечь коллекцию таких объектов. Для Hibernate и DataNucleus данная задача является тривиальной.


Ваша критика статьи станет конструктивной тогда, когда вы предложите свое решение описанной задачи, а не пронесетесь вихрем по комментам с шашкой наголо "так никто не делает, там делов-то на пару строк кода".

ORM в Magento? Его там нет и никогда не было.

Возможно потому, что Magento — не CMS.

Хотел бы подчеркнуть такой момент — все эти телодвижения приводят к тому, что колонки в грид добавляются независимо. Т.е., разработчика модуля не заботит, какие еще модули будут стоять в приложении, и на какие еще таблицы пойдут JOIN'ы.

Information

Rating
Does not participate
Location
Рига, Латвия, Латвия
Date of birth
Registered
Activity

Specialization

Fullstack Developer
Lead
From 3,000 €
JavaScript
HTML
CSS
Node.js
Vue.js
Web development
Progressive Web Apps
PostgreSQL
MySQL
GitHub