Псевдо-инкапсуляция легаси 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).
Приспособленец нам понадобится для запуска и псевдо-инкапсуляции легаси кода, а адаптер — для универсального доступа к нему.
Я сознательно не использую (термин|шаблон)ы: «фасад», «маппер», «декоратор» и тп. Несомненно, в зависимости от того, что содержит и какую структуру имеет легаси-файл (ы) — те или иные (термин|шаблон)ы могут быть более подходящими.
Я ставлю целью относительную универсальность, поэтому подразумеваю, что буду «адаптировать» результат приспособленца под нужды сервиса/модели.
Теперь подробнее о каждом.
Задачи приспособленца в моем случае заключаются в следующем:
- Подменить при необходимости директорию инклудов;
- Подключить необходимый файл;
- Буферизировать результат;
- Инкапсулировать глобальные переменные;
- Псевдо-инкапсулировать глобальные функции;
- Предоставить возможность доступа ко всему вышеперечисленному
Задачи адаптера:
- Настроить и создать приспособленца;
- Дать возможность работать с приспособленцем как с обычным объектом;
- Дать возможность переопределять любые методы и свойства;
- Быть супер-классом для «фасада», «маппера», «декоратора» и других структурных шаблонов
Что получаем в результате:
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 '' . $this->getFlyweight()->call('get_Var2A', [$param1, $param2]); . '';
}
// и тд.
}
И, на мой взгляд, только в зависимости от содержания конечного класса «MyLib» — «адаптер» можно назвать как-то более подходяще.
Так же есть возможность доступа к объявленному внутри файла классу: создание инстанса, получение констант и вызов его статичных методов.
Хотя это можно сделать непосредственно обратившись к нему «по имени» — такая возможность присутствует для абстракции. На тот случай, если после рефактора такой класс перестанет существовать — достаточно будет лишь заменить один метод доступа к нему, а не все вызовы.
И, конечно же, есть ряд недостатков, о которых стоит сказать:
- Глобальные функции и классы продолжают быть глобальными и доступными «напрямую», этот подход только регламентирует доступ к ним, чтобы не «плодить» еще больше зависимого кода;
- Скорость работы. Проведя тест и обратившись к функциям 10 млн. раз — результат был получен за время, вдвое превышающее «нативный» способ. Здесь нужно учитывать нагрузку и оправданность. Хотя, на мой взгляд, в большинстве случаев это не будет существенной проблемой;
- «Синглтонность». Невозможно создать одновременно 2 приспособленца ввиду того, что инклуд можно выполнить только 1 раз
Резюме: если у вас нет нескольких месяцев на рефактор, но есть небольшой запас производительности — думаю вам это может пригодиться: github
Спасибо за внимание.