13 March 2019

Debug Oriented Programming или печаль в глазах Интегратора

PHPProgrammingDebuggingDevelopment for e-commerceMagento

Так получилось, что в последние несколько лет я сшиваю Франкенштейнов, а не ваяю милые фарфоровые статуэтки пастушек и трубочистов. Я создаю решения на базе Magento 2. Это значит, что исходный материал у меня — мечта любого археолога. Культурный слой со следами различных "эпох" и "цивилизаций". По нему можно изучать развитие программистской мысли в PHP/JS сообществах в течение последнего десятилетия.


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


image


Сшить Франкенштейна из такого материала помогает отладчик (debugger). Ниже идёт мой персональный топ приёмов кодирования, которые способны усложнить жизнь любому, кто, как и я, ежедневно использует отладчик в своей жизни. Он небольшой, на четыре позиции, но каждый раз, когда я сталкиваюсь с подобным при отладке — я печалюсь. Может быть мой пост уменьшит количество скорбей в мире, а может и нет. Я, по крайней мере, попытаюсь.


Обфускация и шифрование кода


Это вне конкурса. Я пару раз сталкивался с модулями, для работы которых нужен ionCube, и могу сказать, что последнее, что я сделаю — поставлю в свой проект подобный модуль. Я полностью поддерживаю минификацию JS-кода, особенно, когда рядом идёт обычный исходник, но обфускация и шифрование — дистиллированное, концентрированное зло. Это я вам как Интегратор говорю.


IV. Однострочный код


Экономия на строчках кода — самое безобидное в моем списке:


if ($cond) aaa(); else bbb();

Зависание на два шага на этой строке при пошаговом выполнении программы (вычисление условия, выполнение ветки true или false). Ничего страшного, нужно просто держать в голове, сколько раз ты выполнил step-over на этой строке, и отслеживать значение $cond в списке переменных. Со временем нарабатывается автоматизм.


Чуть хуже то, что ты не можешь поставить безусловную точку останова (breakpoint) на ветке true или false. Придётся вместо одного клика в IDE поработать мышкой/клавиатурой чуть подольше, добавляя условную точку останова.


Идеальный вариант, когда каждый исполняемый шаг (условие, true-ветка, false-ветка) находится на своей строке:


if ($cond)
    aaa();
else
    bbb();

III. Результат-выражение


Использование выражений в условиях:


if (exp()) ...

циклах:


foreach (exp() as $item) ...

в качестве параметров:


foo(exp(), ...)

и возвращаемого результата:


return exp();

не только делает код "плотнее", облегчая его понимание, но и затрудняет отладку — ты просто не видишь значений выполнения выражений в списке переменных отладчика. Приходится добавлять watches (интересный вопрос, а если мониторить генераторы через watches, повлияет ли это на выполнение программы?).


Идеальный вариант — временная переменная:


$exp = exp();
if ($exp) ...

II. Множество точек выхода


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


public function onEvent($event) {
    if($event == 'entrance') {
         return 'entranceRoute';
    } else if($event == 'exit') {
         return 'exitRoute';
    }
     return 'defaultRoute';
}

Вот более правильный вариант:


public function onEvent($event) {
    $result = 'defaultRoute';
    if($event == 'entrance') {
        $result = 'entranceRoute';
    } else if($event == 'exit') {
        $result = 'exitRoute';
    }
    return $result;
}

Всё, мне не нужно разбрасывать точки останова на каждом return или делать step-out с первой строки (если вызывающий код даёт мне возможность посмотреть результат в отдельной переменной), чтобы понять, чем закончилось выполнение. Представляете себе функцию на 120 строк и 22 return'а внутри? А я такую собственноручно дебажил и подозреваю, что это не предел.


I. Каскадный вызов методов


Мой фаворит — method cascading:


$collection
    ->addFilterByProduct(...)
    ->addShowInStoresFilter(...)
    ->addPublicFilter(...)
    ->addApprovedStatusFilter(...)
    ->addCreatedAtLessThanNowFilter(...);

Если мне нужно попасть внутрь метода addApprovedStatusFilter(), который является интерфейсным и имплементирован в нескольких различных классах (конкретный класс определяется во время выполнения), то самое простое — поставить точку останова на $collection и по-очереди проходить всё (addFilterByProduct, addShowInStoresFilter, addPublicFilter) вплоть до нужного места. Если соединить это с использованием выражений в параметрах и возвращаемых результатах, то путь становится совсем не близкий. В оригинале данный код выглядит так:


$collection
    ->addFilterByProduct($this->getProduct())
    ->addShowInStoresFilter($this->_storeManager->getStore()->getId())
    ->addPublicFilter()
    ->addApprovedStatusFilter()
    ->addCreatedAtLessThanNowFilter();

Да, с каскадированием методов код становится более читаемым, но дебажить его становится сложнее. Ничего не имею против каскада set'еров (я, как правило, не дебажу сеттеры):


$answerModel
    ->setAuthorName(...)
    ->setStatus(...)
    ->setCreatedAt(...)
    ->setAuthorEmail(...)
    ->setCustomerId(...)
    ->setHelpfulness(...)

Но код, который содержит логику, который может быть придётся дебажить, а может быть даже самому, лучше писать "олдскульно" (я сам так и делаю):


$collection->addFilterByProduct(...);
$collection->addShowInStoresFilter(...);
$collection->addPublicFilter(...);
$collection->addApprovedStatusFilter(...);
$collection->addCreatedAtLessThanNowFilter(...);

Намного сложнее для восприятия стало?


Или вот рекомендации хорошего стиля программирования:


ladder.up().up().down().up().down().showStep();

Посмотрите на это с точки зрения интегратора, который должен залезть внутрь второго down'а.


Резюме


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


Disclaimer


Изложенное выше является результатом персональной профессиональной деформации и может отличаться от мнений, основанных на других профессиональных деформациях.

Tags:отладкакодированиепрограммированиехороший кодплохой кодпопаболь
Hubs: PHP Programming Debugging Development for e-commerce Magento
+15
5k 16
Comments 43