Рефакторинг legacy кода на PHP: продвинутые техники и стратегии

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Рефакторинг 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), которые делают всё сразу. Используйте паттер

Блоги

Рекомендаций по книгам