Рефакторинг legacy кода на PHP: продвинутый уровень
Работа с устаревшим (legacy) кодом на PHP — неизбежная реальность для многих разработчиков. Часто такой код представляет собой монолитное приложение без тестов, с глобальными зависимостями и смешением бизнес-логики с представлением. Простой рефакторинг «на глаз» может привести к фатальным ошибкам. В этой статье мы рассмотрим продвинутые техники рефакторинга, которые позволят безопасно и эффективно модернизировать legacy-системы.
1. Стратегия «Seam»: создание точек разрыва
Первый шаг в рефакторинге legacy кода — создание швов (seams). Шов — это место, где вы можете изменить поведение кода без его прямого редактирования. Это особенно важно, когда код не покрыт тестами и вы не можете запустить его в изоляции.
Рассмотрим классический пример: прямая работа с базой данных внутри метода.
<?php
// Legacy код
class UserReport {
public function getActiveUsers() {
$db = new PDO('mysql:host=localhost;dbname=test', 'root', '');
$stmt = $db->query("SELECT * FROM users WHERE active = 1");
return $stmt->fetchAll();
}
}
?>
Проблема: new PDO(...) создает жесткую зависимость. Чтобы протестировать этот метод, нужно иметь реальную базу данных.
Решение: внедрение зависимости через конструктор.
<?php
// Рефакторинг: создаем шов через DI
class UserReport {
private PDO $pdo;
public function __construct(PDO $pdo) {
$this->pdo = $pdo;
}
public function getActiveUsers(): array {
$stmt = $this->pdo->query("SELECT * FROM users WHERE active = 1");
return $stmt->fetchAll();
}
}
// Использование
$pdo = new PDO('mysql:host=localhost;dbname=test', 'root', '');
$report = new UserReport($pdo);
?>
Теперь вы можете подменить PDO на мок-объект в тестах. Это простейший, но эффективный шов.
2. Паттерн «Sprout Method» и «Sprout Class»
Когда вы не можете изменить существующий метод (например, из-за отсутствия тестов), используйте стратегию «Sprout» — создавайте новый код рядом со старым, постепенно переключая вызовы.
Пример: у нас есть огромный метод, который валидирует и сохраняет пользователя.
<?php
class UserService {
public function saveUser(array $data): bool {
// 100 строк валидации, логирования и сохранения
$email = $data['email'] ?? '';
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException('Invalid email');
}
// ... много другого кода
$db = new PDO(...);
$stmt = $db->prepare("INSERT INTO users ...");
// ...
}
}
?>
Вместо того чтобы рефакторить весь метод сразу, создайте новый класс для валидации.
<?php
class UserValidator {
public function validate(array $data): void {
if (!filter_var($data['email'] ?? '', FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException('Invalid email');
}
// Добавьте другие проверки
}
}
class UserService {
private UserValidator $validator;
public function __construct(UserValidator $validator) {
$this->validator = $validator;
}
public function saveUser(array $data): bool {
$this->validator->validate($data);
// Остальной код пока остается
}
}
?>
Теперь вы можете написать юнит-тесты для UserValidator, не трогая старый код. Постепенно вы будете выносить логику из saveUser в новые классы.
3. Избавление от глобальных состояний и статики
Legacy код часто использует глобальные переменные, синглтоны и статические методы. Это делает код практически непроверяемым.
Проблема:
<?php
class Logger {
public static function log(string $message): void {
file_put_contents('/var/log/app.log', $message . PHP_EOL, FILE_APPEND);
}
}
class UserController {
public function action() {
// ...
Logger::log('User created');
}
}
?>
Решение: внедрение зависимости через интерфейс.
<?php
interface LoggerInterface {
public function log(string $message): void;
}
class FileLogger implements LoggerInterface {
public function log(string $message): void {
file_put_contents('/var/log/app.log', $message . PHP_EOL, FILE_APPEND);
}
}
class UserController {
private LoggerInterface $logger;
public function __construct(LoggerInterface $logger) {
$this->logger = $logger;
}
public function action() {
// ...
$this->logger->log('User created');
}
}
?>
Теперь в тестах вы можете передать MockLogger, который ничего не пишет в файл, а просто сохраняет сообщения в памяти для проверки.
4. Рефакторинг с помощью «Golden Master» тестов
Когда код настолько запутан, что невозможно понять его логику, используйте подход Golden Master. Вы запускаете приложение с различными входными данными и сохраняете выходные результаты (золотой мастер). Затем вы рефакторите код и проверяете, что результаты не изменились.
Пример для PHP:
<?php
// 1. Запускаем legacy код и сохраняем результат
$legacyResult = legacyFunction($input);
file_put_contents('golden_master/' . md5($input) . '.json', json_encode($legacyResult));
// 2. После рефакторинга сравниваем
$newResult = refactoredFunction($input);
$expected = json_decode(file_get_contents('golden_master/' . md5($input) . '.json'), true);
assert($newResult === $expected, 'Результат изменился!');
?>
Этот метод особенно полезен для рефакторинга функций, которые обрабатывают сложные данные (например, парсинг отчетов, конвертация форматов).
5. Постепенное внедрение Middleware и Pipeline
Legacy код часто содержит «божественные объекты» (God Objects), которые делают всё сразу. Используйте паттер