Псевдо-инкапсуляция легаси include-ов когда нет времени рефакторить

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

    Наиболее частая ситуация, которую я могу привести в пример — str_repeat('очень-', 20) старый код, не знающий даже классов, планируется перенести или частично использовать в современном фреймворке, но переписывать тысячи строк и десятки зависимостей нет времени. Такое бывает, когда заказчик вдруг решает существенно модернизировать или развивать проект, который 10+ лет работал без изменений, а сапортил его один парттайм-олдскул-программист изредка перезагружая пару-тройку сервисов и восстанавливая пароли.

    Должен отметить, что на эту статью меня натолкнуло описание «Garbage Wrapper» от search в комментариях к моей предыдущей статье.

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

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

    Возьмем для примера классический файл той чудной noPSR-эпохи:

    // legacy_lib.php
    
    include("settings.inc.php");
    require("functions.php");
    require_once("database.connection.php");
    
    define('SOME_CONST', 'value');
    
    $var1 = funcName(CONST_2);
    
    function get_Var2A($param1, $param2) {
        return functionFromAnotherInclude($param2, $param1);
    }
    
    class myClass
    {
        var $data = '';
        function getData() {
            global $var1;
            // do somethig
            return get_Var2A($var1, SOME_CONST);
        }
    }
    
    include_once("specialCode.php");
    
    function needThis() {
        $obj = new myClass();
        return unknownFunctionFromInclude() + $obj->getData();
    }
    $var2 = needThis();
    printr('{"param":' . $var2 . '; "var": ' . $var1 . '}');

    На самом деле такой файл часто может достигать 1000+ строк и зависимостей в разы бывает больше.

    Можно попытаться разнести этот код в классы, сервисы и тд. Но вероятность того, что он будет работать так же — устремится к нулю.

    Нужно быстрое решение, которое даст возможность запустить задачу и сделать рефактор «плавнее» ну или вовсе забить отложить его на некоторое время.

    Я не буду применять здесь канонические шаблоны проектирования потому что в таких ситуациях это очень субъективно. Предложу лишь воспользоваться подходами из двух этих: приспособленец (flyweight) и адаптер (adapter).

    Приспособленец нам понадобится для запуска и псевдо-инкапсуляции легаси кода, а адаптер — для универсального доступа к нему.

    Я сознательно не использую (термин|шаблон)ы: «фасад», «маппер», «декоратор» и тп. Несомненно, в зависимости от того, что содержит и какую структуру имеет легаси-файл(ы) — те или иные (термин|шаблон)ы могут быть более подходящими.

    Я ставлю целью относительную универсальность, поэтому подразумеваю, что буду «адаптировать» результат приспособленца под нужды сервиса/модели.

    Теперь подробнее о каждом.

    Задачи приспособленца в моем случае заключаются в следующем:

    1. Подменить при необходимости директорию инклудов;
    2. Подключить необходимый файл;
    3. Буферизировать результат;
    4. Инкапсулировать глобальные переменные;
    5. Псевдо-инкапсулировать глобальные функции;
    6. Предоставить возможность доступа ко всему вышеперечисленному

    Задачи адаптера:

    1. Настроить и создать приспособленца;
    2. Дать возможность работать с приспособленцем как с обычным объектом;
    3. Дать возможность переопределять любые методы и свойства;
    4. Быть супер-классом для «фасада», «маппера», «декоратора» и других структурных шаблонов

    Что получаем в результате:

    class MyLib extends LegacyAbstractAdapter
    {
        /**
         * Configure flyweight
         */
        protected function configure()
        {
            $this
                ->setLegacyFile('legacy_lib.php')
                ->setLegacyPath('/path/to/includes')
            ;
        }
    }
    
    $myLib = new MyLib();
    
    // получаем переменные
    $var1 = $myLib->var1;
    $var2 = $myLib->var2;
    
    // перезаписываем их
    $myLib->var1 = 'some new value';
    
    // доступ к функциям
    $res1 = $myLib->get_Var2A($param1, $param2);
    $res2 = $myLib->needThis();
    
    // получение результата выполнения файла
    $content = $myLib->getFlyweight()->getContent();

    Теперь нам также доступна возможность декорировать, делать композиции и тд.

    class MyLib extends LegacyAbstractAdapter
    {
        /**
         * Configure flyweight
         */
        protected function configure()
        {
            $this
                ->setLegacyFile('legacy_lib.php')
                ->setLegacyPath('/path/to/includes')
            ;
        }
        
        // переопределенная функция
        public function needThis()
        {
            return 'dummy value';
        }
        
        // декорирование функции
        public function get_Var2A($param1, $param2)
        {
            return '<font>' . $this->getFlyweight()->call('get_Var2A', [$param1, $param2]); . '</font>';
        }
        
        // и тд.
    }

    И, на мой взгляд, только в зависимости от содержания конечного класса «MyLib» — «адаптер» можно назвать как-то более подходяще.

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

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

    И, конечно же, есть ряд недостатков, о которых стоит сказать:

    • Глобальные функции и классы продолжают быть глобальными и доступными «напрямую», этот подход только регламентирует доступ к ним, чтобы не «плодить» еще больше зависимого кода;
    • Скорость работы. Проведя тест и обратившись к функциям 10 млн. раз — результат был получен за время, вдвое превышающее «нативный» способ. Здесь нужно учитывать нагрузку и оправданность. Хотя, на мой взгляд, в большинстве случаев это не будет существенной проблемой;
    • «Синглтонность». Невозможно создать одновременно 2 приспособленца ввиду того, что инклуд можно выполнить только 1 раз

    Резюме: если у вас нет нескольких месяцев на рефактор, но есть небольшой запас производительности — думаю вам это может пригодиться: github

    Спасибо за внимание.
    • +16
    • 5,9k
    • 1
    Поделиться публикацией

    Комментарии 1

      0
      Bесьма интересный подход. Наверное попробую использовать в одном легаси проэкте. Спасибо!

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

      Самое читаемое