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

в 13:57, , рубрики: adapter, flyweight, legacy, php, refactoring, ненормальное программирование, ооп, Программирование, Проектирование и рефакторинг

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

Наиболее частая ситуация, которую я могу привести в пример — 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

Спасибо за внимание.

Автор: jced

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js